v0.0.42

4 July 2026

Named-parameter callable dispatch, per-page session scoping, config-driven addon load order, TemplateFields base+type merge, and functions.php cleanup.

Added

  • Invoke::call(callable $fn, array $available) : calls a callable by matching $available values to its parameters by name, in whatever order the callable declares them, skipping any it doesn't ask for. Controllers, routers, and hooks can now declare only the parameters they need, in any order.
  • Page::session() : returns a session scope auto-namespaced to the page's view name. AddonPage overrides this to scope to the addon name (the part of the id after /), so all of an addon's views share one scope.
  • TemplateFields::values(App $app, array $defs, array $current, string $path) : static helper that builds a FieldValueSet from resolved defs and current values. Shared between Field::optionFields() and FieldValue::optionFields() to avoid duplicating the build loop.
  • FieldTypeNumber::encode() : coerces the saved value to numeric; empty string saves as null instead of being stored as-is.

Changed

  • Addon loading is now config-driven: addons load in the order they appear in config.json addons, with no filesystem directory scan. Config order is the only load-order guarantee.
  • TemplateFields::resolve() now merges the base template (fieldtypes/index.json) with the type-specific template (fieldtypes/{type}/index.json) : fields from the base apply to every type unless the type template overrides them.
  • Record::templateFieldDefs() renamed to Record::templateFields().
  • App::send() now accepts an optional ?string $block parameter : honoring the :block URL suffix the same way a normal GET does.
  • DirectSessionAccessRule extended to also check scope() and make() call sites against configured allowed file paths, enforcing scope ownership by filename.
  • functions.php : q() aliased to a new query(), p() aliased to a new page(), find() added as a shorthand for page('/')->find($query). csrf(), isLoggedIn(), and fieldtypeEditTemplate() removed.