| 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 | | | |