├── .phpstorm.meta.php ├── composer.json ├── license.md ├── readme.md └── src └── ComponentModel ├── ArrayAccess.php ├── Component.php ├── Container.php ├── IComponent.php ├── IContainer.php └── RecursiveComponentIterator.php /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@'])); 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nette/component-model", 3 | "description": "⚛ Nette Component Model", 4 | "keywords": ["nette", "components"], 5 | "homepage": "https://nette.org", 6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "https://davidgrudl.com" 11 | }, 12 | { 13 | "name": "Nette Community", 14 | "homepage": "https://nette.org/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "8.1 - 8.4", 19 | "nette/utils": "^4.0" 20 | }, 21 | "require-dev": { 22 | "nette/tester": "^2.5", 23 | "tracy/tracy": "^2.9", 24 | "phpstan/phpstan-nette": "^2.0@stable" 25 | }, 26 | "autoload": { 27 | "classmap": ["src/"], 28 | "psr-4": { 29 | "Nette\\": "src" 30 | } 31 | }, 32 | "minimum-stability": "dev", 33 | "scripts": { 34 | "phpstan": "phpstan analyse", 35 | "tester": "tester tests -s" 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "4.0-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | Good news! You may use Nette Framework under the terms of either 5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3. 6 | 7 | The BSD License is recommended for most projects. It is easy to understand and it 8 | places almost no restrictions on what you can do with the framework. If the GPL 9 | fits better to your project, you can use the framework under this license. 10 | 11 | You don't have to notify anyone which license you are using. You can freely 12 | use Nette Framework in commercial projects as long as the copyright header 13 | remains intact. 14 | 15 | Please be advised that the name "Nette Framework" is a protected trademark and its 16 | usage has some limitations. So please do not use word "Nette" in the name of your 17 | project or top-level domain, and choose a name that stands on its own merits. 18 | If your stuff is good, it will not take long to establish a reputation for yourselves. 19 | 20 | 21 | New BSD License 22 | --------------- 23 | 24 | Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com) 25 | All rights reserved. 26 | 27 | Redistribution and use in source and binary forms, with or without modification, 28 | are permitted provided that the following conditions are met: 29 | 30 | * Redistributions of source code must retain the above copyright notice, 31 | this list of conditions and the following disclaimer. 32 | 33 | * Redistributions in binary form must reproduce the above copyright notice, 34 | this list of conditions and the following disclaimer in the documentation 35 | and/or other materials provided with the distribution. 36 | 37 | * Neither the name of "Nette Framework" nor the names of its contributors 38 | may be used to endorse or promote products derived from this software 39 | without specific prior written permission. 40 | 41 | This software is provided by the copyright holders and contributors "as is" and 42 | any express or implied warranties, including, but not limited to, the implied 43 | warranties of merchantability and fitness for a particular purpose are 44 | disclaimed. In no event shall the copyright owner or contributors be liable for 45 | any direct, indirect, incidental, special, exemplary, or consequential damages 46 | (including, but not limited to, procurement of substitute goods or services; 47 | loss of use, data, or profits; or business interruption) however caused and on 48 | any theory of liability, whether in contract, strict liability, or tort 49 | (including negligence or otherwise) arising in any way out of the use of this 50 | software, even if advised of the possibility of such damage. 51 | 52 | 53 | GNU General Public License 54 | -------------------------- 55 | 56 | GPL licenses are very very long, so instead of including them here we offer 57 | you URLs with full text: 58 | 59 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) 60 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Nette Component Model 2 | ===================== 3 | 4 | [![Downloads this Month](https://img.shields.io/packagist/dm/nette/component-model.svg)](https://packagist.org/packages/nette/component-model) 5 | [![Tests](https://github.com/nette/component-model/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nette/component-model/actions) 6 | [![Coverage Status](https://coveralls.io/repos/github/nette/component-model/badge.svg?branch=master)](https://coveralls.io/github/nette/component-model?branch=master) 7 | [![Latest Stable Version](https://poser.pugx.org/nette/component-model/v/stable)](https://github.com/nette/component-model/releases) 8 | [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/component-model/blob/master/license.md) 9 | 10 | 11 | Introduction 12 | ------------ 13 | 14 | Components are the foundation of reusable code. They make your work easier and allow you to profit from community work. Components are wonderful. 15 | Nette Framework introduces several classes and interfaces for all these types of components. 16 | 17 | Documentation can be found on the [website](https://doc.nette.org/component-model). 18 | 19 | If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you! 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | The recommended way to install is via Composer: 26 | 27 | ``` 28 | composer require nette/component-model 29 | ``` 30 | 31 | It requires PHP version 8.1 and supports PHP up to 8.4. 32 | -------------------------------------------------------------------------------- /src/ComponentModel/ArrayAccess.php: -------------------------------------------------------------------------------- 1 | addComponent($component, $name); 30 | } 31 | 32 | 33 | /** 34 | * Returns component specified by name. Throws exception if component doesn't exist. 35 | * @param string|int $name 36 | * @return T 37 | * @throws Nette\InvalidArgumentException 38 | */ 39 | public function offsetGet($name): IComponent 40 | { 41 | $name = is_int($name) ? (string) $name : $name; 42 | return $this->getComponent($name); 43 | } 44 | 45 | 46 | /** 47 | * Does component specified by name exists? 48 | * @param string|int $name 49 | */ 50 | public function offsetExists($name): bool 51 | { 52 | $name = is_int($name) ? (string) $name : $name; 53 | return $this->getComponent($name, throw: false) !== null; 54 | } 55 | 56 | 57 | /** 58 | * Removes component from the container. 59 | * @param string|int $name 60 | */ 61 | public function offsetUnset($name): void 62 | { 63 | $name = is_int($name) ? (string) $name : $name; 64 | if ($component = $this->getComponent($name, throw: false)) { 65 | $this->removeComponent($component); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ComponentModel/Component.php: -------------------------------------------------------------------------------- 1 | 20 | * @property-read string $name 21 | * @property-read T|null $parent 22 | */ 23 | abstract class Component implements IComponent 24 | { 25 | use Nette\SmartObject; 26 | 27 | private ?IContainer $parent = null; 28 | private ?string $name = null; 29 | 30 | /** @var array}> means [type => [obj, depth, path, [attached, detached]]] */ 31 | private array $monitors = []; 32 | 33 | 34 | /** 35 | * Finds the closest ancestor of specified type. 36 | * @param bool $throw throw exception if component doesn't exist? 37 | * @return ($throw is true ? IComponent : ?IComponent) 38 | */ 39 | final public function lookup(?string $type, bool $throw = true): ?IComponent 40 | { 41 | if (!isset($this->monitors[$type])) { // not monitored or not processed yet 42 | $obj = $this->parent; 43 | $path = self::NameSeparator . $this->name; 44 | $depth = 1; 45 | while ($obj !== null) { 46 | $parent = $obj->getParent(); 47 | if ($type ? $obj instanceof $type : $parent === null) { 48 | break; 49 | } 50 | 51 | $path = self::NameSeparator . $obj->getName() . $path; 52 | $depth++; 53 | $obj = $parent; // IComponent::getParent() 54 | if ($obj === $this) { 55 | $obj = null; // prevent cycling 56 | } 57 | } 58 | 59 | if ($obj) { 60 | $this->monitors[$type] = [$obj, $depth, substr($path, 1), []]; 61 | 62 | } else { 63 | $this->monitors[$type] = [null, null, null, []]; // not found 64 | } 65 | } 66 | 67 | if ($throw && $this->monitors[$type][0] === null) { 68 | $message = $this->name !== null 69 | ? "Component '$this->name' is not attached to '$type'." 70 | : "Component of type '" . static::class . "' is not attached to '$type'."; 71 | throw new Nette\InvalidStateException($message); 72 | } 73 | 74 | return $this->monitors[$type][0]; 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::NAME_SEPARATOR. 81 | * @return ($throw is true ? string : ?string) 82 | */ 83 | final public function lookupPath(?string $type = null, bool $throw = true): ?string 84 | { 85 | $this->lookup($type, $throw); 86 | return $this->monitors[$type][2]; 87 | } 88 | 89 | 90 | /** 91 | * Starts monitoring ancestors for attach/detach events. 92 | */ 93 | final public function monitor(string $type, ?callable $attached = null, ?callable $detached = null): void 94 | { 95 | if (!$attached && !$detached) { 96 | throw new Nette\InvalidStateException('At least one handler is required.'); 97 | } 98 | 99 | if ( 100 | ($obj = $this->lookup($type, throw: false)) 101 | && $attached 102 | && !in_array([$attached, $detached], $this->monitors[$type][3], strict: true) 103 | ) { 104 | $attached($obj); 105 | } 106 | 107 | $this->monitors[$type][3][] = [$attached, $detached]; // mark as monitored 108 | } 109 | 110 | 111 | /** 112 | * Stops monitoring ancestors of specified type. 113 | */ 114 | final public function unmonitor(string $type): void 115 | { 116 | unset($this->monitors[$type]); 117 | } 118 | 119 | 120 | /********************* interface IComponent ****************d*g**/ 121 | 122 | 123 | final public function getName(): ?string 124 | { 125 | return $this->name; 126 | } 127 | 128 | 129 | /** 130 | * Returns the parent container if any. 131 | * @return T 132 | */ 133 | final public function getParent(): ?IContainer 134 | { 135 | return $this->parent; 136 | } 137 | 138 | 139 | /** 140 | * Sets or removes the parent of this component. This method is managed by containers and should 141 | * not be called by applications 142 | * @param T $parent 143 | * @throws Nette\InvalidStateException 144 | * @internal 145 | */ 146 | public function setParent(?IContainer $parent, ?string $name = null): static 147 | { 148 | if ($parent === null && $this->parent === null && $name !== null) { 149 | $this->name = $name; // just rename 150 | return $this; 151 | 152 | } elseif ($parent === $this->parent && $name === null) { 153 | return $this; // nothing to do 154 | } 155 | 156 | // A component cannot be given a parent if it already has a parent. 157 | if ($this->parent !== null && $parent !== null) { 158 | throw new Nette\InvalidStateException("Component '$this->name' already has a parent."); 159 | } 160 | 161 | // remove from parent? 162 | if ($parent === null) { 163 | $this->refreshMonitors(0); 164 | $this->parent = null; 165 | 166 | } else { // add to parent 167 | $this->validateParent($parent); 168 | $this->parent = $parent; 169 | if ($name !== null) { 170 | $this->name = $name; 171 | } 172 | 173 | $tmp = []; 174 | $this->refreshMonitors(0, $tmp); 175 | } 176 | 177 | return $this; 178 | } 179 | 180 | 181 | /** 182 | * Validates the new parent before it's set. 183 | * Descendant classes can override this to implement custom validation logic. 184 | * @param T $parent 185 | * @throws Nette\InvalidStateException 186 | */ 187 | protected function validateParent(IContainer $parent): void 188 | { 189 | } 190 | 191 | 192 | /** 193 | * Refreshes monitors. 194 | * @param array|null $missing (array = attaching, null = detaching) 195 | * @param array $listeners 196 | */ 197 | private function refreshMonitors(int $depth, ?array &$missing = null, array &$listeners = []): void 198 | { 199 | if ($this instanceof IContainer) { 200 | foreach ($this->getComponents() as $component) { 201 | if ($component instanceof self) { 202 | $component->refreshMonitors($depth + 1, $missing, $listeners); 203 | } 204 | } 205 | } 206 | 207 | if ($missing === null) { // detaching 208 | foreach ($this->monitors as $type => $rec) { 209 | if (isset($rec[1]) && $rec[1] > $depth) { 210 | if ($rec[3]) { // monitored 211 | $this->monitors[$type] = [null, null, null, $rec[3]]; 212 | foreach ($rec[3] as $pair) { 213 | $listeners[] = [$pair[1], $rec[0]]; 214 | } 215 | } else { // not monitored, just randomly cached 216 | unset($this->monitors[$type]); 217 | } 218 | } 219 | } 220 | } else { // attaching 221 | foreach ($this->monitors as $type => $rec) { 222 | if (isset($rec[0])) { // is in cache yet 223 | continue; 224 | 225 | } elseif (!$rec[3]) { // not monitored, just randomly cached 226 | unset($this->monitors[$type]); 227 | 228 | } elseif (isset($missing[$type])) { // known from previous lookup 229 | $this->monitors[$type] = [null, null, null, $rec[3]]; 230 | 231 | } else { 232 | unset($this->monitors[$type]); // forces re-lookup 233 | if ($obj = $this->lookup($type, throw: false)) { 234 | foreach ($rec[3] as $pair) { 235 | $listeners[] = [$pair[0], $obj]; 236 | } 237 | } else { 238 | $missing[$type] = true; 239 | } 240 | 241 | $this->monitors[$type][3] = $rec[3]; // mark as monitored 242 | } 243 | } 244 | } 245 | 246 | if ($depth === 0) { // call listeners 247 | $prev = []; 248 | foreach ($listeners as $item) { 249 | if ($item[0] && !in_array($item, $prev, strict: true)) { 250 | $item[0]($item[1]); 251 | $prev[] = $item; 252 | } 253 | } 254 | } 255 | } 256 | 257 | 258 | /********************* cloneable, serializable ****************d*g**/ 259 | 260 | 261 | /** 262 | * Object cloning. 263 | */ 264 | public function __clone() 265 | { 266 | if ($this->parent === null) { 267 | return; 268 | 269 | } elseif ($this->parent instanceof Container) { 270 | $this->parent = $this->parent->_isCloning(); 271 | if ($this->parent === null) { // not cloning 272 | $this->refreshMonitors(0); 273 | } 274 | } else { 275 | $this->parent = null; 276 | $this->refreshMonitors(0); 277 | } 278 | } 279 | 280 | 281 | /** 282 | * Prevents serialization. 283 | */ 284 | final public function __sleep() 285 | { 286 | throw new Nette\NotImplementedException('Object serialization is not supported by class ' . static::class); 287 | } 288 | 289 | 290 | /** 291 | * Prevents unserialization. 292 | */ 293 | final public function __wakeup() 294 | { 295 | throw new Nette\NotImplementedException('Object unserialization is not supported by class ' . static::class); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/ComponentModel/Container.php: -------------------------------------------------------------------------------- 1 | 20 | * @property-read T[] $components 21 | */ 22 | class Container extends Component implements IContainer 23 | { 24 | private const NameRegexp = '#^[a-zA-Z0-9_]+$#D'; 25 | 26 | /** @var IComponent[] */ 27 | private array $components = []; 28 | private ?Container $cloning = null; 29 | 30 | 31 | /********************* interface IContainer ****************d*g**/ 32 | 33 | 34 | /** 35 | * Adds a child component to the container. 36 | * @throws Nette\InvalidStateException 37 | */ 38 | public function addComponent(IComponent $component, ?string $name, ?string $insertBefore = null): static 39 | { 40 | if ($name === null) { 41 | $name = $component->getName(); 42 | if ($name === null) { 43 | throw new Nette\InvalidStateException("Missing component's name."); 44 | } 45 | } 46 | 47 | if (!preg_match(self::NameRegexp, $name)) { 48 | throw new Nette\InvalidArgumentException("Component name must be non-empty alphanumeric string, '$name' given."); 49 | } 50 | 51 | if (isset($this->components[$name])) { 52 | throw new Nette\InvalidStateException("Component with name '$name' already exists."); 53 | } 54 | 55 | // check circular reference 56 | $obj = $this; 57 | do { 58 | if ($obj === $component) { 59 | throw new Nette\InvalidStateException("Circular reference detected while adding component '$name'."); 60 | } 61 | 62 | $obj = $obj->getParent(); 63 | } while ($obj !== null); 64 | 65 | // user checking 66 | $this->validateChildComponent($component); 67 | 68 | if (isset($this->components[$insertBefore])) { 69 | $tmp = []; 70 | foreach ($this->components as $k => $v) { 71 | if ((string) $k === $insertBefore) { 72 | $tmp[$name] = $component; 73 | } 74 | 75 | $tmp[$k] = $v; 76 | } 77 | 78 | $this->components = $tmp; 79 | } else { 80 | $this->components[$name] = $component; 81 | } 82 | 83 | try { 84 | $component->setParent($this, $name); 85 | } catch (\Throwable $e) { 86 | unset($this->components[$name]); // undo 87 | throw $e; 88 | } 89 | 90 | return $this; 91 | } 92 | 93 | 94 | /** 95 | * Removes a child component from the container. 96 | */ 97 | public function removeComponent(IComponent $component): void 98 | { 99 | $name = $component->getName(); 100 | if (($this->components[$name] ?? null) !== $component) { 101 | throw new Nette\InvalidArgumentException("Component named '$name' is not located in this container."); 102 | } 103 | 104 | unset($this->components[$name]); 105 | $component->setParent(null); 106 | } 107 | 108 | 109 | /** 110 | * Retrieves a child component by name or creates it if it doesn't exist. 111 | * @param bool $throw throw exception if component doesn't exist? 112 | * @return ($throw is true ? IComponent : ?IComponent) 113 | */ 114 | final public function getComponent(string $name, bool $throw = true): ?IComponent 115 | { 116 | [$name] = $parts = explode(self::NameSeparator, $name, 2); 117 | 118 | if (!isset($this->components[$name])) { 119 | if (!preg_match(self::NameRegexp, $name)) { 120 | if ($throw) { 121 | throw new Nette\InvalidArgumentException("Component name must be non-empty alphanumeric string, '$name' given."); 122 | } 123 | 124 | return null; 125 | } 126 | 127 | $component = $this->createComponent($name); 128 | if ($component && !isset($this->components[$name])) { 129 | $this->addComponent($component, $name); 130 | } 131 | } 132 | 133 | $component = $this->components[$name] ?? null; 134 | if ($component !== null) { 135 | if (!isset($parts[1])) { 136 | return $component; 137 | 138 | } elseif ($component instanceof IContainer) { 139 | return $component->getComponent($parts[1], $throw); 140 | 141 | } elseif ($throw) { 142 | throw new Nette\InvalidArgumentException("Component with name '$name' is not container and cannot have '$parts[1]' component."); 143 | } 144 | } elseif ($throw) { 145 | $hint = Nette\Utils\ObjectHelpers::getSuggestion(array_merge( 146 | array_map('strval', array_keys($this->components)), 147 | array_map('lcfirst', preg_filter('#^createComponent([A-Z0-9].*)#', '$1', get_class_methods($this))), 148 | ), $name); 149 | throw new Nette\InvalidArgumentException("Component with name '$name' does not exist" . ($hint ? ", did you mean '$hint'?" : '.')); 150 | } 151 | 152 | return null; 153 | } 154 | 155 | 156 | /** 157 | * Creates a new component. Delegates creation to createComponent method if it exists. 158 | */ 159 | protected function createComponent(string $name): ?IComponent 160 | { 161 | $ucname = ucfirst($name); 162 | $method = 'createComponent' . $ucname; 163 | if ( 164 | $ucname !== $name 165 | && method_exists($this, $method) 166 | && (new \ReflectionMethod($this, $method))->getName() === $method 167 | ) { 168 | $component = $this->$method($name); 169 | if (!$component instanceof IComponent && !isset($this->components[$name])) { 170 | $class = static::class; 171 | throw new Nette\UnexpectedValueException("Method $class::$method() did not return or create the desired component."); 172 | } 173 | 174 | return $component; 175 | } 176 | 177 | return null; 178 | } 179 | 180 | 181 | /** 182 | * Returns all immediate child components. 183 | * @return array 184 | */ 185 | final public function getComponents(): iterable 186 | { 187 | $filterType = func_get_args()[1] ?? null; 188 | if (func_get_args()[0] ?? null) { // back compatibility 189 | $iterator = new RecursiveComponentIterator($this->components); 190 | $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); 191 | if ($filterType) { 192 | $iterator = new \CallbackFilterIterator($iterator, fn($item) => $item instanceof $filterType); 193 | } 194 | return $iterator; 195 | } 196 | 197 | return $filterType 198 | ? array_filter($this->components, fn($item) => $item instanceof $filterType) 199 | : $this->components; 200 | } 201 | 202 | 203 | /** 204 | * Retrieves the entire hierarchy of components, including all nested child components (depth-first). 205 | * @return list 206 | */ 207 | final public function getComponentTree(): array 208 | { 209 | $res = []; 210 | foreach ($this->components as $component) { 211 | $res[] = $component; 212 | if ($component instanceof self) { 213 | $res = array_merge($res, $component->getComponentTree()); 214 | } 215 | } 216 | return $res; 217 | } 218 | 219 | 220 | /** 221 | * Validates a child component before it's added to the container. 222 | * Descendant classes can override this to implement custom validation logic. 223 | * @throws Nette\InvalidStateException 224 | */ 225 | protected function validateChildComponent(IComponent $child): void 226 | { 227 | } 228 | 229 | 230 | /********************* cloneable, serializable ****************d*g**/ 231 | 232 | 233 | /** 234 | * Handles object cloning. Clones all child components and re-sets their parents. 235 | */ 236 | public function __clone() 237 | { 238 | if ($this->components) { 239 | $oldMyself = reset($this->components)->getParent(); 240 | assert($oldMyself instanceof self); 241 | $oldMyself->cloning = $this; 242 | foreach ($this->components as $name => $component) { 243 | $this->components[$name] = clone $component; 244 | } 245 | 246 | $oldMyself->cloning = null; 247 | } 248 | 249 | parent::__clone(); 250 | } 251 | 252 | 253 | /** 254 | * Is container cloning now? 255 | * @internal 256 | */ 257 | final public function _isCloning(): ?self 258 | { 259 | return $this->cloning; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/ComponentModel/IComponent.php: -------------------------------------------------------------------------------- 1 | 43 | */ 44 | function getComponents(): iterable; 45 | } 46 | -------------------------------------------------------------------------------- /src/ComponentModel/RecursiveComponentIterator.php: -------------------------------------------------------------------------------- 1 | current() instanceof IContainer; 25 | } 26 | 27 | 28 | /** 29 | * The sub-iterator for the current element. 30 | */ 31 | public function getChildren(): self 32 | { 33 | return new self($this->current()->getComponents()); 34 | } 35 | 36 | 37 | /** 38 | * Returns the count of elements. 39 | */ 40 | public function count(): int 41 | { 42 | return iterator_count($this); 43 | } 44 | } 45 | --------------------------------------------------------------------------------