v0.0.41

3 July 2026

Field/FieldValue split, Session/SessionRoot scoped-access refactor, class renames throughout, Collections namespace, Res::ensureCacheControl backstop, and cache addon.

Added

  • FieldValue class : the per-render counterpart to Field. Field is now definition-only (type, label, options); FieldValue carries the raw value, decodes it, renders it, and validates it. Record::__get now returns FieldValue instead of Field.
  • FieldValueSet : ordered set of FieldValue instances; returned by Page::fields() and Field::optionFields(). Replaces FieldCollection in rendering contexts.
  • TemplateFields : static utility class backing Record::resolveTemplate()/templateFieldDefs() and FieldValue::optionFields(), shared so non-Record value objects can reuse the same template resolution logic.
  • SessionRoot : the full session implementation. Session is now a thin prefix-scoped wrapper; $app->session is SessionRoot::scope('site'). Use $app->session->scope('name') for addon-namespaced sessions. NamespacedSession is removed.
  • HttpException : base class for all typed HTTP exceptions (NotFoundException, ForbiddenException, etc.). App::execute() now only trusts HTTP status codes from this hierarchy; a stray \RuntimeException with an arbitrary code defaults to 500.
  • DirectSessionAccessRule : PHPStan rule enforcing that all session data access goes through a named scope rather than directly on $app->session.
  • phpstan.neon : PHPStan config added to the repo.
  • cache addon : purges Souin cache on page.update:after, page.delete:before, file.update:after, and file.delete:after via a PURGE request to the Caddy admin API.
  • Http::purge(string $url, array $headers) : PURGE HTTP method support.
  • Res::hasHeader(string $name) : checks whether a named response header has already been sent.
  • Res::ensureCacheControl() : sets Cache-Control: no-store if no Cache-Control header is queued. Called as a last-chance backstop in all Res::redirect/html/json/text and Hx::navigate/location/refresh exit paths so no response can accidentally fall through to Souin's heuristic caching default.
  • V::allowedExtension(string $filename, array $allowed) : validates a filename's extension against an allowed list (case-insensitive, no leading dot).
  • Page::upload(array $file, array $options) : validates the uploaded file's extension against config.media.allowed, sanitizes the filename, and stores it via PageFiles.
  • User::role() : returns the UserRole instance for this user's role.
  • Registry::countRows(string $query) : count variant of rows() that returns a row count without loading page objects.
  • Record::templateRoot() : new hook; subclasses can override just this (returning e.g. 'fieldtypes') instead of overriding template() directly.

Changed

  • Renames: AddonAddonPage, AddonMountsAddonRegistry, EmailMailer, PermissionUserRole, ViewPageView.
  • All collection classes moved to the Phlat\Collections\ namespace (RecordCollection, PageCollection, UserCollection, FieldCollection, AddonCollection).
  • Field::metaFields() renamed to Field::optionFields() (returns FieldValueSet).
  • Page::fragment readonly property → Page::fragment() method; now lazy (reads the view template on demand instead of at construction).
  • Page::tail() removed (was an alias for remainder()).
  • Page::siblings() and Page::children() accept a $query string as first parameter.
  • Page::find() no longer has a $recursive parameter.
  • Page::fileSourcePath() changed from protected to public.
  • PageView (View renamed) gains PHP 8.4 property hooks for title, description, and cache.
  • F::mime() : extension override map added for CSS, JS, JSON, and SVG, since mime_content_type() cannot distinguish text-based formats.
  • DataFile::write() now pretty-prints JSON.
  • Registry find()/rows()/countRows() all route through a single internal compile() method instead of duplicating SQL assembly.
  • Admin media view removed; the browse view replaces it with a tabbed picker/files interface.
  • Non-image public files now served directly from Page::route().