nette/component-model
nette/component-model provides foundational classes for hierarchical component trees in Nette apps. It handles named parent-child relationships, lifecycle events, component lookup, and recursive iteration, enabling reusable, loosely coupled UI and application components.
1. Install (standalone, not a Laravel package)
composer require nette/component-model:^3.2
There is no service provider, no config file, no facade. nette/component-model is a ~4-file library (Component, Container, IComponent, ArrayAccess trait). It does not auto-discover in Laravel and does not need to — you pull it into your own service classes. Requires PHP 8.1+.
2. Alias the imports immediately
Before writing any code, settle on a convention, because Container and Component collide with both Illuminate and Livewire:
use Nette\ComponentModel\Container as ComponentContainer;
use Nette\ComponentModel\Component as ComponentNode;
3. Read these first
readme.md — the canonical API tour; this is more useful than the API docs.src/Component.php — getName(), getParent(), monitor(), lookup().src/Container.php — addComponent(), getComponent(), getComponents(), getComponentTree(), NAME_REGEXP.release/v3.2.0.md — the deprecation list you must code against today so 4.0 doesn't break you.4. First use case: a hierarchical domain tree inside a Laravel service
The natural Laravel fit is not UI rendering — it's a nestable domain structure with lifecycle hooks. A navigation menu tree is the smallest useful demo:
use Nette\ComponentModel\Container as ComponentContainer;
use Nette\ComponentModel\Component as ComponentNode;
class MenuNode extends ComponentNode
{
public function __construct(
private string $label,
private string $url = '#',
) {}
}
final class MenuTree extends ComponentContainer {}
final class MenuBuilder
{
public function build(): MenuTree
{
$root = new MenuTree();
$main = new MenuNode('Products', '/products');
$root->addComponent($main, 'products');
$main->addComponent(new MenuNode('Widgets', '/products/widgets'), 'widgets');
$root->addComponent(new MenuNode('About', '/about'), 'about');
return $root;
}
}
Retrieval mirrors the hierarchy:
$tree->getComponent('products'); // direct child
$tree->getComponent('products/widgets'); // path lookup across the tree
$tree->getComponentTree(); // ['products' => ['widgets' => []], 'about' => []]
Bind the tree in a service provider if you inject it elsewhere — but see the gotchas about scope before you make it a singleton.
Pattern 1 — Use it as a data-structure layer, not a rendering layer
The component model has no rendering, no HTML, no Blade hooks, no reactivity. Keep it in a plain PHP service, build the tree, then hand a flattened array to Blade/Livewire:
// MenuBuilder::toArray() — flatten for a Blade partial
public function toArray(ComponentContainer $node): array
{
return array_map(fn ($child) => [
'name' => $child->getName(),
'label' => $child->label,
'url' => $child->url,
'children' => $child instanceof ComponentContainer ? $this->toArray($child) : [],
], $node->getComponents());
}
This pattern gives you Laravel-side flexibility (cache the array, serialize to JSON, feed Inertia) while keeping tree logic in a tested, type-hinted place.
Pattern 2 — Lifecycle hooks with monitor()
monitor() is the modern replacement for subclassing attached() / detached(). Register listeners on a container, and they fire as children are added/removed:
$root->monitor(MenuNode::class,
fn (MenuNode $node) => Log::debug('attached', ['name' => $node->getName()]),
fn (MenuNode $node) => Log::debug('detached', ['name' => $node->getName()]),
);
Typical day-to-day uses: audit logging of tree mutations, invalidating a cache key when a subtree grows, warming a lookup table, or notifying a UI state store. If you need cross-service communication, dispatch a Laravel event from inside the closure — do not reach for Nette's listener machinery further than that.
Pattern 3 — Path-based lookup for routing and permissions
getComponent('a/b/c') traverses the tree by name. This is handy for permission checks against a nested resource tree (e.g. organizations/acme/billing/invoices) or for resolving a breadcrumb chain from a path string. Combine with getComponentTree() for an easy "list all nodes" endpoint used by an admin panel or a Telescope-style debug view.
Pattern 4 — Wrap it, don't extend, in application code
Subclass Container once per domain concept (CategoryTree, FormSectionTree), then inject the builder, not the tree, via Laravel's DI:
// AppServiceProvider
$this->app->singleton(CategoryTreeBuilder::class);
// Controller / Action
public function __construct(private CategoryTreeBuilder $builder) {}
The builder itself is a plain Laravel service; the Nette classes are implementation details.
Pattern 5 — Enforce naming conventions
Container::NAME_REGEXP is provided. Override addComponent() in your tree subclasses to validate names (slugs, numeric keys, etc.) rather than validating at the call site. Remember addComponent($component, $name) accepts int/string names via ArrayAccess but Container::getComponent() accepts only strings.
Pattern 6 — Use the ArrayAccess trait on your wrappers
Nette\ComponentModel\ArrayAccess gives $container['name'] semantics for free. Useful when you want the tree to feel natural to Laravel developers who are used to array access, without exposing the base class everywhere.
Name collision is the #1 footgun. Nette\ComponentModel\Container is not Illuminate\Container\Container, and Nette\ComponentModel\Component is not Livewire\Component. Always alias. If IDE autocomplete suggests the wrong one, you'll get a very confusing "call to undefined method getComponent()" at runtime.
Do not confuse the two containers conceptually. Nette's Container is a tree node registry; Laravel's is a DI container. Do not try to bind Nette's container as a Laravel singleton and then inject it as $app->make(Container::class) — it will 500 in subtle ways. Keep them on separate names.
Magic properties are deprecated in 3.2.0 and removed in 4.0. $component->name, $component->parent, $container->components — replace with getName(), getParent(), getComponents() today. Same for IComponent::NAME_SEPARATOR → IComponent::NameSeparator.
attached() / detached() overrides are on the way out. 3.2.0 deprecates the old monitor($type) without callbacks and the subclass hook pattern. Migrate to monitor($type, $attached, $detached) now; it normalizes callbacks to \Closure, which matters below.
Closure identity for dedup. Callbacks are deduplicated by loose comparison. Passing the same closure instance twice registers once; passing two identical-but-distinct closures can register twice if the engine's loose comparison treats them the same. If you see double-fire in production, hoist your callbacks to properties or static methods.
Re-entrancy in refreshMonitors(). Since 3.1.4, refreshMonitors() refuses to re-enter itself while listeners are running. A listener that removes and re-adds the same component won't loop — but also won't do what you might expect. Do not mutate the tree inside a monitor callback unless it's the only clear way; prefer queueing the mutation outside the listener.
getComponents($deep) is deprecated. In 3.1.0 the $deep parameter was silently deprecated: when false it returns an array (a BC break from the previous iterator behavior). Always call getComponents() with no arguments in new code and iterate the returned collection. Use getComponentTree() when you actually need nesting.
One parent, one name. A component can be attached to only one container, and names must be unique within a parent. Attempting to add a component that already has a parent throws — the exception messages are informative (since 3.0.0, and 2.2.1 improved the "component has parent" message), so read them rather than guess.
No Laravel integrations to expect. No auto-discovery, no config, no facade, no Telescope integration, no Blade tag. If you need observability, wrap getComponentTree() in your own debug endpoint or dump it into a Laravel log entry.
Serialization is awkward by design. Parent and child hold references to each other. Avoid serialize()-ing a tree directly; build a DTO/array (Pattern 1) or implement __sleep/__wakeup carefully. This bites especially when caching a built tree in Redis.
Performance: traversal is linear and fine for hundreds to low thousands of nodes. If you're walking the full tree on every request, flatten it once and cache the array; don't re-walk in a Blade loop.
Debugging order of operations: when a node "isn't there," check (a) the name passed to addComponent(), (b) whether the parent you're querying is actually the parent of the node you think it is, (c) whether you're using the path form (a/b) vs the direct lookup. getComponentTree() is the fastest way to see the actual shape. Exception messages also suggest close matches (since 2.2.4), so read them.
Upgrade posture: the 4.0 release will remove the deprecations above. Budget a migration when you see it. There are zero Laravel dependents listed, so you're effectively the maintainer of this integration for your app — pin ^3.2 and read release/*.md on upgrade.
Extension points to prefer: subclass Container for typed factories, use the ArrayAccess trait for ergonomic wrappers, and use monitor() as the single point of cross-cutting hookup. Avoid reaching into internal $parent / $components state — that's the path that hits the 4.0 wall first.
How can I help you explore Laravel packages today?