Framework

Repository Author Commit message Committed SHA  
 
component-model by dg added AGENTS.md & DOCS
component-model by dg Component: setParent() rolls the change back when a listener throws

An exception from a monitor listener is a veto (Form forbids nested forms by
throwing from an attached handler), but setParent() left the change half done:

- a failed attach kept the parent set, so after addComponent() undid the
insertion the component pointed at a container that did not list it;
- a failed detach left the component in the tree with part of its monitors
notified as detached, never followed by attached again.

setParent() now rolls back by running the opposite walk: a failed attach is
detached again and gets its parent and name back, a failed detach is attached
again. The walk reuses the depth gate, so it notifies exactly the listeners the
failed walk reached, and works on the live tree, so changes a listener made are
kept. It goes to the end even past handlers that throw during it: notify()
drops their exceptions and calls the next handler, so every listener reached
gets its counterpart, and the veto is what gets thrown.

A listener that moved the component, before throwing or during the rollback,
keeps the move, and nothing more is rolled back; the move counter tells it,
checked again after the rollback. addComponent() likewise undoes its insertion
unless a listener has attached the component there again under that name; a
component refused for already having a parent here under another name still
gets the new entry dropped.

A veto still calls the other handlers of the type, so the rollback balances
all of them, and a handler that moves the component and then throws still gets
the type told where it has ended up before the exception goes on.

A collect-then-notify design was rejected: it would commit the change before
the listeners run, which breaks the veto Form relies on, and contradicts the
single-pass walk listeners are allowed to mutate the tree from. Chaining a
failure of the rollback to the veto, by throwing it from a finally block, was
rejected as a trick a reader easily misses.

component-model by dg Component: monitor state, handlers and lookup cache are kept apart

One $monitors entry per type served as the lookup cache, the handler registry
and the record of what the handlers had been told. That made lookup() lie and
handlers disappear:

- the cached root went stale when an ancestor was attached, so lookup(null)
returned the former top of the subtree;
- a clone answered lookup() with the ancestors of the original, because the
cache was copied along;
- a handler that moved its own component during attached corrupted the entry:
the walk took the callbacks out while the handlers ran, a nested change saw a
type without listeners and dropped it, and putting the callbacks back wrote an
entry holding nothing but them ("Undefined array key", wrong lookup()).

The handlers now live in a registry only monitor() and unmonitor() change. The
monitor state is the depth of the ancestor each type was notified about as
attached, set before attached and removed before detached; a depth rather than
the object, so it holds in a clone too. The walks find ancestors uncached and
read the state live, since a handler may cut off an ancestor and the nested walk
then updates the very node being processed.

The lookup cache is a cache only. The ancestors of a node change only when a
node on its path to the root changes its parent, so every change of the parent
goes through changeParent(), which drops the cache of the whole subtree: after
the parent is set on attach, after it is cleared on detach (the detached
handlers run with the parent still set), and in __clone() for every re-homed
child and for a standalone clone, so not even a lookup made by a __clone()
override of a child, while the clone was still attached above, survives.

Measured on a prototype against 4.0.1: lookup() about 20 % faster, lookupPath()
about 20 % slower, rendering with lazily created components about 15 % faster,
moving a subtree about 13 % slower for the extra pass. Dropping the cache
globally on every change (a generation counter) made rendering with lazily
created components slower than 4.0.1; no cache at all made lookup() 3.8 times
and lookupPath() 7 times slower.

The handlers of a type are told about a change they make to their own component
only once they return: the type is muted on that component while they run, so
they are not called again in the middle, the state still follows the tree, and
afterwards, if the component has ended up under another ancestor, all of them
get detached from the old one and attached from the new one, so the pairs match
for every handler of the type, whichever of them moved it. The walk leaves a
component a handler has moved alone, and setParent(null) does not clear a
parent a handler has set meanwhile. A move is told by a counter changeParent()
increments, never by comparing the parent, as a handler may remove the
component and add it back to the very same container; for the same reason
removeComponent() leaves the array alone when the component is attached there
again. A handler may also add a child to a container being cut off, so the
detach walk scans the children again until none is left unvisited, also after
the container's own handlers. A walk revisits a component a handler has put back
since it was processed, the duplicate check holds only until the component
moves, and attached handlers are compared with the live ancestor whenever they
return, as one of them may have cut off an ancestor.

A detached handler now finds the ancestor being left through lookup(), as it
runs before the parent is cleared; it used to get null.

assets by dg updated upgrading.md
assets by dg AssetsLatte\Runtime: CSP nonce is no longer cached across requests
assets by dg AssetsLatte\Runtime: n:asset rejects an entry point with dependencies instead of dropping them
assets by dg NAssetNode: n:asset no longer collides with n:class on the same element (#9)
assets by vojtechrichter AudioAsset, GenericAsset, VideoAsset: added crossorigin property
assets by dg ViteMapper: JS entry without dependencies is rendered as module [Closes #5]
assets by dg AssetsLatte\Runtime: non-numeric width/height no longer crashes

completeDimensions() fed the raw HTML attribute value into arithmetic, so
<img n:asset=foo width="auto"> ended in "TypeError: Unsupported operand types"
and width="50%" produced a warning plus a nonsense height. Non-numeric values
now skip the ratio computation, so the asset's own width and height are
dropped as with any other override.

assets by dg ViteMapper: clear error when the manifest is missing an imported chunk
component-model by dg Container: cloning state is private, _isCloning() removed (BC break)

While a container cloned its children, it exposed that fact through a public
`_isCloning()` method so each child's __clone() could reattach to the clone
instead of detaching. Two problems: an @internal method with an underscore was
still part of the public API and showed up in IDE completion on every Form and
Presenter, and the flag was cleared after the loop without try/finally. A child
whose __clone() threw left the original container permanently marked as "being
cloned", so every later clone of any of its children silently reattached to the
discarded, half-built clone.

The flag is now a private property on Component, cleared in a finally block. It
has to live on Component because Component::__clone() reads it on another
instance ($this->parent->cloning) and a private member is only reachable from
the scope of the class that declares it. Container writes it through a closure
bound to the Component scope rather than a protected setter: any non-private
method on Component shares its name with methods of every user component, and a
final one would be a fatal error for each that declares a method of that name.
PHPStan does not see the write through Closure::bind, hence the ignore.

_isCloning() was @internal and no call site outside this library exists.

component-model by dg Container: removeComponent() detaches the component before dropping it

The component was dropped from $components before setParent(null) ran. Detached
listeners therefore saw a half-removed tree: the component still had the
container as its parent, but the container no longer listed it. And if a
listener threw, the container stayed that way for good.

The order is now reversed, mirroring addComponent(), which lists the component
before setParent() runs. Listeners in both directions see the component fully in
the tree, as the "called before detaching" contract of monitor() says, and a
throwing listener leaves the component where it was, so there is nothing to undo.
Restoring it afterwards was rejected: it puts the component at the end of the
array, and no position is right once a listener has changed the tree.

As the listeners now run while the component is still listed, one of them may
remove it itself and put another one under the same name, so the entry is
dropped only if it still holds that component.

assets by dg FilesystemMapper: guard against path traversal

The reference was concatenated onto basePath without normalization, so
getAsset('../../../composer.json') resolved to a file outside basePath and
produced a working asset for it. References whose '..' segments lead out of
the base path now throw AssetNotFoundException, including ones like
'../images/x.png' that used to reach a sibling directory; such files belong
to a mapping scope of their own.

assets by dg AudioAsset: duration of a non-MP3 file is null instead of an exception

guessMP3Duration() was called for any audio file, so reading the readonly
$duration property of a .wav/.ogg/.flac asset threw "Failed to find MP3 frame
sync bits". Duration estimation is only implemented for MP3, so it is now
attempted only for audio/mpeg. A corrupt .mp3 still throws.

assets by dg Helpers: createAssetFromUrl() no longer fails on explicit mimeType

ScriptAsset and StyleAsset have no $mimeType constructor parameter, but the
caller-supplied 'mimeType' key was spread into them, causing
"Error: Unknown named parameter $mimeType".

Also recognizes text/javascript, application/ecmascript and text/ecmascript
as scripts; previously only application/javascript matched and a mapper
returning the modern text/javascript got a GenericAsset (which is not
HtmlRenderable and thus fails to render in templates).

assets by dg DIExtension: mapper can be referenced as @service in mapping

A bare service reference like `cloud: @s3mapper` in the assets mapping now
resolves to that service used directly as the mapper (previously it was
mistaken for a path and failed). Entity notation `@s3mapper()` and class
names keep working. Matches the documented 'Service reference' format.

assets by dg AssetNode: outside HTML text print only the escaped asset URL

{asset} and {preload} used to render the complete HTML element in every
escaper state except html/attr, so inside <style> or <script> contents and in
css/js/text/xml templates they injected a whole <link>/<script> element into
the code, breaking it:

<style>
.hero { background: url('{asset 'bg.jpg'}'); }
/* url('<link rel="preload" href=...>') */
</style>

Inside an attribute they printed the URL through %raw, bypassing Latte's
escaping, so a URL containing a quote could break out of the attribute.

Now the element is rendered only in the text of an HTML template and
everywhere else the URL is printed through the compiler's escaper, exactly
like a plain Latte variable: escaped for the attribute or XML, CSS-escaped
inside <style>, and JS-escaped inside <script>, where the escaper supplies
the quotes itself, so the form is {asset foo} without surrounding quotes.

php-generator by dg uses attribute Deprecated
php-generator by dg removed deprecated stuff
php-generator by dg Type::nullable() follows the object model

An intersection becomes the DNF type (A&B)|null instead of throwing,
null stays in 'null' and 'mixed' instead of throwing, and the result
is spelled the way Nette\Utils\Type prints it. The input is now
validated: an empty or invalid string throws Nette\InvalidArgumentException.

php-generator by dg removed deprecated Helpers::validateType() and UPPERCASE constants of Type
php-generator by dg Property::getValue() no longer returns by reference (BC break)

The reference let a value change outside the setters, so a null written
through it never reached the type and printed public int $x = null;

php-generator by dg types are Nette\Utils\Type objects, nullability is part of the type (BC break)

Parameter, Property, Constant and the function return type store their
type as a Nette\Utils\Type object, and nullability is part of it.

- getType(), getReturnType() and Constant::getType() return ?Type; the
declaration string is (string) $type and the $asObject parameter is gone
- setType() and setReturnType() accept Type|string|null; subclasses
overriding setType(?string) must widen the parameter
- setNullable() and setReturnNullable() throw Nette\InvalidStateException
when no type is set yet, setNullable(false) as well
- a null default value or a null initial value is folded into the type;
a later setDefaultValue(5) does not take the null back
- a repeated setType() replaces the whole type including nullability:
setType('Foo')->setNullable()->setType('Bar') gives Bar
- isNullable() is true for int|null, mixed and null
- printing normalizes the spelling: int|null and null|int print as ?int
- Printer::printType(?Type) instead of printType(?string, bool)
- attribute names are printed through simplifyName()
- Factory sets the type of a property after setInitialized()

utils by dg Type: built-in names mixed and null are compared case-insensitively

PHP type names are case-insensitive, but Type recognized only the
lowercase spelling:

- Type::fromString('MIXED')->with('null') produced '?MIXED', which PHP
rejects, because allows() did not treat MIXED as mixed
- Type::fromString('NULL|int') stayed 'NULL|int' and was not recognized
as the simple nullable type '?int'

fromString('?mixed') now gives 'mixed', as fromReflection() does.

php-generator by dg opened 5.0-dev
php-generator by dg setNullable() before setType() and Helpers::validateType() deprecated

Calling Parameter::setNullable() or Property::setNullable() before a type
is set, and setReturnNullable() before setReturnType(), triggers
a deprecation notice; set the type first. Helpers::validateType() is
deprecated in favor of Nette\Utils\Type::fromString(). In 5.0 these
setters throw an exception instead.

component-model by dg Component: monitor() deduplicates detached handlers on registration

An attached handler was already deduplicated when registered, a detached one
was appended again on every monitor() call for the same type. Calling it twice
therefore grew the list indefinitely. Firing was unaffected (refreshMonitors()
deduplicates per callback+object anyway), so this only fixes the registry.

component-model by dg Component: root lookup is cached under a private sentinel key

lookup(null) means "find the root" and its result was cached in $monitors
under an empty string. But the empty string is also a possible user input:
$type is a plain string, class-string is only a phpDoc promise.

The two roles collided. monitor('') looked up '' as a falsy type, which meant
"the root", so it found the root ancestor, immediately invoked the attached
handler with it, and overwrote the cached root lookup. The next attach then
tripped assert($type !== '').

The root lookup is now keyed by self::RootKey ("\0root"), which cannot collide
with a class name, and lookup() switches on $type === null instead of on the
truthiness of the string. An empty (or otherwise unknown) type is therefore an
ordinary type that never matches: $ancestor instanceof '' is simply false, so
monitor('') creates a dead monitor, exactly like a typo in a class name.

Validating the type in monitor() via class_exists() was rejected: a component
may legitimately monitor a type from an optional dependency (as
Forms\Controls\CsrfProtection once monitored Application\UI\Presenter, although
nette/forms does not depend on nette/application), and such a check would throw
a fatal error in a standalone setup.

php-generator by dg PhpFile: addNamespace() no longer silently replaces a non-empty namespace
php-generator by dg PhpFile::addNamespace() & removeNamespace() - passing objects is deprecated (BC break)
php-generator by dg Dumper: added support for preserving array references

Last synchronization: 2026-10-27 08:02:49