diff --git a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
index 26707d954..07aeabcf5 100644
--- a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
+++ b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
@@ -990,9 +990,9 @@ An exceptionally large shared work unit may receive its own linked detail plan w
This compact index routes the completed-work history that must be consulted with the full plan after compaction. Detailed history remains in the [companion ledger](2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md).
-- **Active package or work unit:** None. `routing` is complete; detail plan `2026-08-05-2352-routing-correctness-current-parity-and-cache-lifecycles.md`.
-- **Ledger entries required for the active work:** None. The completed Routing work is recorded under `Complete Routing correctness, current parity, and cache lifecycles`, with `redis-24` and `collections-14` cross-referenced from the owning Redis and Collections entries.
-- **Pending revalidation carried into the active work:** None. `contracts-05`, `reflection-02`, `container-08`, `support-02`, and `routing-01` are revalidated.
+- **Active package or work unit:** None.
+- **Ledger entries required for the active work:** None.
+- **Pending revalidation carried into the active work:** None.
Update these three lines when a package starts, completes, or gains a cross-package dependency. Name exact work-unit headings or shared finding IDs from the companion ledger; never use “see recent entries” or require a full-ledger reread.
@@ -1003,7 +1003,7 @@ Add one row only for a shared finding or changed lower-level assumption that ano
| Finding | Owning package | Affected or revalidation packages | Ledger entry |
|---|---|---|---|
| `validation-01` | `validation` | `contracts` and `validation` (revalidation complete) | `Harden framework contracts and request-scoped state`; shared finding `validation-01` |
-| `view-01` | `view` | `contracts` and `foundation` (revalidation complete); later full `view` audit | `Harden framework contracts and request-scoped state`; shared finding `view-01` |
+| `view-01` | `view` | `contracts`, `foundation`, and `view` (revalidation complete) | `Harden framework contracts and request-scoped state`; shared finding `view-01` |
| `filesystem-01` | `filesystem` | `contracts` and `filesystem` (revalidation complete) | `Harden framework contracts and request-scoped state`; shared finding `filesystem-01` |
| `queue-01` | `queue` | `contracts` and `queue` (revalidation complete) | `Harden framework contracts and request-scoped state`; shared finding `queue-01` |
| `contracts-05` | `contracts` | `http`, `foundation`, `console`, `database`, and `routing` (revalidation complete) | `Harden framework contracts and request-scoped state`; finding `contracts-05` |
@@ -1011,7 +1011,7 @@ Add one row only for a shared finding or changed lower-level assumption that ano
| `http-01` | `http` | `macroable` and `http` (revalidation complete), `testing`; later full `testing` audit | `Complete Macroable callable and test-state handling`; shared finding `http-01` |
| `console-01` | `console` | `contracts` and `console` (revalidation complete) | `Preserve typed console contracts during Composer scripts`; shared finding `console-01` |
| `reflection-01` | `reflection` | `events` and `foundation` (revalidation complete) | `Consolidate reflection metadata and correct callable inference`; finding `reflection-01` |
-| `reflection-02` | `reflection` | `foundation`, `console`, and `routing` (revalidation complete); later full `view` audit | `Consolidate reflection metadata and correct callable inference`; finding `reflection-02` |
+| `reflection-02` | `reflection` | `foundation`, `console`, `routing`, and `view` (revalidation complete) | `Consolidate reflection metadata and correct callable inference`; finding `reflection-02` |
| `reflection-04` | `reflection` | `di` and `queue` (revalidation complete), `support`, `testing`; later full consumer audits | `Consolidate reflection metadata and correct callable inference`; finding `reflection-04` |
| `config-01` | `config` | `foundation` (revalidation complete) | `Preserve configuration identity across worker reloads`; finding `config-01` |
| `config-02` | `foundation` | `reverb` (revalidation complete), `testing`; later full `testing` audit | `Preserve configuration identity across worker reloads`; finding `config-02` |
@@ -1175,6 +1175,11 @@ Add one row only for a shared finding or changed lower-level assumption that ano
| `routing-18` | `routing` | `auth` (revalidation complete) | `Complete Routing correctness, current parity, and cache lifecycles`; finding `routing-18` |
| `collections-14` | `collections` | `collections` and `routing` (revalidation complete) | `Complete Routing correctness, current parity, and cache lifecycles`; finding `collections-14` |
| `validation-18` | `validation` | `validation` and `support` (revalidation complete) | `Complete Validation correctness, parity, and compiled lifecycles`; finding `validation-18` |
+| `view-09` | `foundation` | `foundation` and `view` (revalidation complete) | `Complete View correctness, lifecycle, and current parity`; finding `view-09` |
+| `view-24` | `foundation` | `foundation` and `view` (revalidation complete) | `Complete View correctness, lifecycle, and current parity`; finding `view-24` |
+| `view-37` | `view` | `view` (revalidation complete), `foundation`, `testbench`, and `testing` (targeted corrections complete); later full `testbench` and `testing` audits | `Complete View correctness, lifecycle, and current parity`; finding `view-37` |
+| `view-38` | `view` | `view` (revalidation complete), `boost` (targeted correction complete); later full `boost` audit | `Complete View correctness, lifecycle, and current parity`; finding `view-38` |
+| `translation-10` | `translation` | `view` (sibling revalidation complete); later full `translation` audit | `Complete View correctness, lifecycle, and current parity`; finding `translation-10` |
## Package checklist
@@ -1262,7 +1267,7 @@ The order is lower-level first where practical. Hypervel has cross-cutting depen
- [x] `auth`
- [x] `validation`
- [x] `routing`
-- [ ] `view`
+- [x] `view`
- [ ] `translation`
- [ ] `pagination`
- [ ] `socialite`
diff --git a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
index fa8c69d5f..d66fdda72 100644
--- a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
+++ b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
@@ -92,7 +92,7 @@ Append package entries in checklist order. Keep each entry compact but complete
| `notifications-07` | Contract defect | Major | High | The Notification Factory rejects a supported single notifiable even though every implementation and the Dispatcher accepts it | Widen Factory notifiables to `mixed`, retain its two-parameter `sendNow()`, and keep channels on the Dispatcher contract |
- **Important rejected concerns:** Do not rewrite validation around only `ValidationRule`; Laravel's deprecated contracts remain its live internal execution protocol, userland already receives the modern API, and a rewrite would add permanent synchronization cost without behavior gain. Do not remove `Console\Application::add()`: Symfony deprecated its underlying method, but Laravel deliberately retains, uses, and tests a non-deprecated wrapper over `addCommand()`. Do not add orphan Image contracts before Hypervel has a coherent Image package. Do not relocate the Monolog-specific context contract without a separately approved API redesign. Do not add optional dependencies merely because they appear only in lazy parameter/return types. Do not make public `View::share()` request-dependent or add a public request-sharing API. Do not keep a throwing `Request::get()` tombstone; record the intentional omission so static analysis rejects it.
-- **Cross-package implications:** `validation-01` affects `validation`; `view-01` affects `view` and `foundation`; `filesystem-01` affects `filesystem`; `queue-01` affects `queue`; `contracts-04` affects `server`, `server-process`, `websocket-server`, and `reverb`; `contracts-05` affects `http`, `routing`, `foundation`, `console`, and `database`; `contracts-09` affects Foundation and Broadcasting; `contracts-10` affects Mail; `contracts-11` affects Mail and Console; and `notifications-07` affects Notifications. Queue, Foundation, Broadcasting, HTTP, Mail, Notifications, and the affected Console consumer revalidation are complete; the later HTTP work also completed the Contracts-owned part of `routing-01` by restoring the URL generator's open parameter-normalization contract. The remaining package checkboxes stay open until their own complete audits.
+- **Cross-package implications:** `validation-01` affects `validation`; `view-01` affects `view` and `foundation`; `filesystem-01` affects `filesystem`; `queue-01` affects `queue`; `contracts-04` affects `server`, `server-process`, `websocket-server`, and `reverb`; `contracts-05` affects `http`, `routing`, `foundation`, `console`, and `database`; `contracts-09` affects Foundation and Broadcasting; `contracts-10` affects Mail; `contracts-11` affects Mail and Console; and `notifications-07` affects Notifications. Queue, Foundation, Broadcasting, HTTP, Mail, Notifications, View, and the affected Console consumer revalidation are complete. The later HTTP work also completed the Contracts-owned part of `routing-01` by restoring the URL generator's open parameter-normalization contract. The remaining package checkboxes stay open until their own complete audits.
- **Implementation:** Declared every external parent-interface dependency in the split package; added evidence-backed contract and implementation types; ported `ShouldBeDiscovered` with current upstream source, fixtures, tests, and docs; corrected the handshake/process spellings without aliases; removed only APIs directly deprecated by Laravel plus dead migration wiring; retained Laravel's live `Application::add()` wrapper; normalized every statically valid `resolveCommands()` argument shape; corrected Engine diagnostics; completed contract docs and upstream references; and added lifecycle warnings to concrete worker-state mutators. The later Queue work removed the optional broadcaster capability and widened Factory notifiables without changing Factory `sendNow()` arity or the Dispatcher channel boundary. The validation, view, filesystem, and queue corrections below were implemented at their owning boundaries, with superseded state writes, loose construction types, unrestorable identifier conversion, raw morph-alias restoration, dead dependencies, obsolete tests, and stale comments removed.
- **Regression tests:** Added split-package dependency-presence coverage, discovery opt-out fixtures, exact Engine diagnostic coverage inside and outside a coroutine, custom-filesystem result validation, configured-rule clone/isolation coverage, deterministic concurrent view-error isolation, ordinary serialization for non-Eloquent queue contracts, morph-mapped Eloquent collection restoration, and all supported scalar/array `resolveCommands()` forms. Updated every affected contract implementation and test double, including integration-only caster fixtures; the later Queue gate verifies the broadcaster command and Notification implementers against the corrected contracts.
- **Later Mail revalidation:** `contracts-10` aligns every Mail queue entry point with the framework's enum-capable identifier boundary while retaining non-nullable `queueOn()` and `laterOn()` aliases. `contracts-11` replaces unsupported `mixed` callback metadata with `Closure|string`; focused Mail and Console scheduling coverage proves the narrowed contract through the real consumer path without changing runtime behavior.
@@ -118,8 +118,8 @@ Append package entries in checklist order. Keep each entry compact but complete
- **Failure:** Concurrent middleware scopes overwrite `Factory::$shared['errors']`, allowing one request to render another request's validation errors.
- **Decision:** Keep public `share()` as a boot-time worker baseline. Store only the request overlay in a minimal internal coroutine-local state owner with exact nested/exceptional restoration. `Factory::mergeSharedData()` composes global, request, and local data in one merge; `shared()` and `getShared()` observe the overlay. No public Factory-contract expansion or scoped Factory clone.
- **Implementation and cleanup:** Added the minimal coroutine-local `RequestSharedData` owner. Session-error middleware now scopes its overlay for exactly the downstream request lifetime and restores prior state in `finally`. Factory render/shared reads compose global, request, and local data with the intended precedence; the singleton error write and middleware constructor dependency were removed.
-- **Validation:** Precedence, nested exceptional restoration, and deterministic two-request interleaving regressions pass; the work-unit review is signed off and the later full `view` and `foundation` audits remain pending.
-- **Revalidation:** Full `view` and `foundation` audits remain pending.
+- **Validation:** Precedence, nested exceptional restoration, and deterministic two-request interleaving regressions pass; the work-unit review and complete Foundation and View audits are signed off.
+- **Revalidation:** The complete Foundation and View audits retained the request overlay, merge precedence, exact restoration, and worker-baseline contract without change.
### Shared finding `filesystem-01`: truthful filesystem construction boundary
@@ -248,11 +248,12 @@ Append package entries in checklist order. Keep each entry compact but complete
| `reflection-06` | Improvement | Improvement | High | Public `lazy()` and `proxy()` helpers were ported without their current upstream runtime tests, type assertions, or user documentation | Port the current distinct behavior/type coverage and add proportionate helper documentation |
- **Important rejected concerns:** Keep `object|string` on the shared metadata APIs: removing one measurable approximately 7–12 nanosecond normalization branch does not justify narrowing a useful shared API. Retain Laravel's deliberate `Exception`-only attribute-constructor fallback and null caching; programming `Error`s must still surface and remain uncached. Do not add relative-type resolution, reflection locks, recursive nested-trait discovery, a general callable rewrite, guards around invalid lazy-helper overload combinations, a package README without package-specific content, or a framework-wide conversion of unrelated reflection construction.
-- **Cross-package implications:** `reflection-01` affects `events` and `foundation`; `reflection-02` affects `console`, `routing`, `view`, and `foundation`; `reflection-03` affects `container`; `reflection-04` affects `container`, `di`, `support`, `queue`, and `testing`; `reflection-05` adds accurate exception documentation in `database`, `events`, and `routing`. Queue revalidation is complete; the remaining package checkboxes stay open until their own complete audits.
+- **Cross-package implications:** `reflection-01` affects `events` and `foundation`; `reflection-02` affects `console`, `routing`, `view`, and `foundation`; `reflection-03` affects `container`; `reflection-04` affects `container`, `di`, `support`, `queue`, and `testing`; `reflection-05` adds accurate exception documentation in `database`, `events`, and `routing`. Queue, Foundation, Console, Routing, and View revalidation are complete. The remaining package checkboxes stay open until their own complete audits.
- **Approved implementation boundary:** The owner approved the canonical metadata owner, deletion of the redundant internal managers, the evidence-backed public class-target narrowing, and the public helper documentation. Preserve the flexible `object|string` cache API and separate class/method arrays. Route only the existing duplicate-manager consumers and scoped same-package reflection sites through the cache; do not convert unrelated framework reflection calls. Add no lock, context state, registry, compatibility layer, or relative-type resolver.
- **Implementation:** `ClassMetadataCache` now owns canonical class and method metadata, and the duplicate container and DI managers, their dead APIs/tests, and redundant test-state resets are removed. Existing manager consumers and the scoped reflection-package sites use the canonical cache. Callable-array validation, first-parameter inference, relative return-type rejection, class-target types, exception annotations, and proxy callback documentation now match their verified behavior. The current `lazy()`/`proxy()` runtime, type, and user-documentation surfaces are complete.
- **Regression tests:** Focused coverage reproduces the invalid-first-parameter, malformed-callable-array, and unresolved-`parent` failures; preserves valid union, callable, and attribute-fallback behavior; verifies canonical class/method identity and reset semantics; and covers current `lazy()`/`proxy()` runtime and PHPStan inference behavior.
- **Later Routing revalidation:** The complete Routing audit revalidated `reflection-02` through route callable and controller metadata paths without changing Reflection source.
+- **Later View revalidation:** The complete View audit revalidated `reflection-02` through stringable closure inference and component/compiler reflection paths without changing Reflection source.
- **Performance and complexity:** The consolidation is a net deletion and retains lock-free worker-static metadata reads. Separate canonical class/method caches benchmark at least as well as the duplicate managers. Existing `object|string` normalization remains for API flexibility; no new hot-path work is introduced. Closure-first-parameter handling becomes simpler and cheaper by removing collection traversal.
- **Laravel-facing result:** Supported Laravel-facing runtime call shapes and configuration remain unchanged. Current upstream `lazy()` and `proxy()` behavior and documentation are complete, and narrowed types reject only invalid inputs.
- **Validation and review:** Focused Reflection, Container, DI, Events, Routing, Eloquent, and Foundation tests are green. Dedicated helper runtime and PHPStan type coverage, `composer analyse`, the complete `composer fix` gate, `git diff --check`, stale-reference scans, package-checklist parity, fresh self-review, independent code review, and owner pre-commit approval are complete.
@@ -1031,7 +1032,7 @@ Append package entries in checklist order. Keep each entry compact but complete
- **Approved owner gates:** The owner approved the process-local/testing-only array maintenance driver, nullable redirect and current Laravel API additions, the truthful `Http\Kernel` contract expansion required by middleware configuration, the `0600` default for newly decrypted plaintext, and correcting verified upstream defects rather than preserving parity bugs. The Kernel contract change is documented for custom implementations. The existing worker maintenance wrapper remains a periodic same-process snapshot and does not make the array driver cross-process.
- **Important rejected concerns:** Do not add shutdown-callback registries, PHPUnit meta-fixtures, a generic finalizer, request-scoped Application/Vite clones, Carbon managers, locks, watchers, retry loops, PID incarnation tracking, publication/delete transaction services, arbitrary-stream copy transactions, SQL parsers, TTY emulation, or broad PHPStan impurity annotations. The supported paths are covered by existing lifecycle, coroutine-context, Filesystem replacement, Process, and typed-reflection primitives. Retain `Filesystem::delete()` cache clearing because the full caller set and PHP stat cache require it; use targeted cache invalidation only where raw command/native postconditions demand it.
-- **Cross-package implications and revalidation:** Foundation revalidated all carried `view-01`, `testbench-01`, `reflection-01`, `reflection-02`, `config-01`, `config-02`, `context-01`, `context-04`, `coroutine-06`, `foundation-02`, `concurrency-01`, `concurrency-03`, `di-02`, `http-02`, `filesystem-07`, `foundation-04`, `events-01`, `events-04`, `events-06`, `foundation-01`, `support-02`, `encryption-03`, `server-process-10`, `bus-03`, `bus-17`, `bus-18`, `core-01`, and `core-05` assumptions. The later Queue work completed `queue-14` and revalidated Foundation's canonical base queue configuration under `queue-29`. The Reverb work completed `reverb-24` by adding the supported server path to Foundation's canonical broadcasting client options with focused config coverage; no Foundation runtime assumption changed. The Scout work adds `foundation-17` and `foundation-18`: Scout delegates to Foundation's truthful Meilisearch waiter, deletes its duplicate Support implementation, uses a service-valid custom key with exact remote precondition coverage, and inherits exact Meilisearch and Algolia cleanup-task ownership. The later HTTP work completed `http-03` and Foundation's helper side of `routing-01`, restoring open URL parameter forwarding without adding conversion at the helper boundary. `foundation-06` requires later full Testbench revalidation; `auth-02` requires the Auth audit; and `database-03` requires the Database and Testbench audits.
+- **Cross-package implications and revalidation:** Foundation revalidated all carried `view-01`, `testbench-01`, `reflection-01`, `reflection-02`, `config-01`, `config-02`, `context-01`, `context-04`, `coroutine-06`, `foundation-02`, `concurrency-01`, `concurrency-03`, `di-02`, `http-02`, `filesystem-07`, `foundation-04`, `events-01`, `events-04`, `events-06`, `foundation-01`, `support-02`, `encryption-03`, `server-process-10`, `bus-03`, `bus-17`, `bus-18`, `core-01`, and `core-05` assumptions. The later Queue work completed `queue-14` and revalidated Foundation's canonical base queue configuration under `queue-29`. The Reverb work completed `reverb-24` by adding the supported server path to Foundation's canonical broadcasting client options with focused config coverage; no Foundation runtime assumption changed. The Scout work adds `foundation-17` and `foundation-18`: Scout delegates to Foundation's truthful Meilisearch waiter, deletes its duplicate Support implementation, uses a service-valid custom key with exact remote precondition coverage, and inherits exact Meilisearch and Algolia cleanup-task ownership. The later HTTP work completed `http-03` and Foundation's helper side of `routing-01`, restoring open URL parameter forwarding without adding conversion at the helper boundary. The View work completes Foundation-owned `view-09` and `view-24` by publishing the canonical compiler defaults and correcting `view:cache` root deduplication with focused Foundation coverage. Its `view-37` follow-up documents why exception-handler restoration remains caller-driven while the global subscriber owns ordinary framework static-state cleanup. `foundation-06` requires later full Testbench revalidation; `auth-02` requires the Auth audit; and `database-03` requires the Database and Testbench audits.
- **Upstream and documentation:** Originating Laravel implementation and documentation pull requests supplied discovery history; current local Laravel default-branch source, tests, metadata, and docs supplied the porting reference. Hypervel deliberately retains its Swoole exception renderer, coroutine-aware test lifecycle, worker-cached maintenance model, and checked TTY behavior. Authentication, configuration, database testing, deployment, HTTP testing, middleware, and queue documentation now covers every new public call shape and important runtime limitation without exposing internal choreography.
- **Implementation:** Foundation/Testbench teardown now exhausts every independent owner and clears state exactly once. External-service clients, global clocks/environment, and dump recursion are restored at their actual lifetimes. Maintenance drivers, middleware, commands, Vite, renderers, clear/link/cache/environment commands, generators, publishers, and installers check native failures and publish only validated state. Current Laravel dispatch, retry-stopping, preferred-JSON, health, HTTP testing, route-list, nullable redirect, and database-testing APIs are complete; the narrow retry callback upstream defect is corrected. Dead duplicate Scout setup, stale helpers/properties/imports, unsafe raw boundaries, false success paths, and obsolete comments are removed.
- **Regression tests:** Deterministic coverage injects failure into every teardown/termination phase, setup wrapper shape, SDK/clock/dumper/environment restoration, maintenance/cache/file publication, generator/installer/publisher operation, reflection/Blade/native read, health state, retry callback typing, and lazy database refresh boundary. It also covers coroutine isolation, process-local array semantics, current Laravel public API call shapes, custom dispatch construction, Kernel contract consumers, mode preservation, old-artifact survival, and exact throwable precedence.
@@ -1805,3 +1806,38 @@ Append package entries in checklist order. Keep each entry compact but complete
- **Laravel-facing result:** Supported Validation signatures, named arguments, Factory/Validator behavior, protected extension paths, FormRequest responses, facade methods, and rule APIs remain compatible or are restored to current Laravel. The deliberate strict-membership and malformed-date differences remove coercive or nondeterministic behavior rather than preserving a known defect through compatibility machinery.
- **Validation and review:** Every changed test file and the complete Validation unit/integration group are green. The authoritative `composer fix` gate passed formatting, both PHPStan configurations, the full parallel suite, Testbench package mode, and dogfood. `git diff --check`, metadata/facade regeneration, stale-symbol scans, and fresh caller/callee, lifecycle, API, hot-path, dead-code, overengineering, and independent code review are complete.
- **Assessment:** The implementation fixes each accepted finding at its owning boundary and removes stale metadata and unsafe fast-path assumptions without adding speculative machinery. No accepted defect, TODO, workaround, unintended Laravel API break, or meaningful performance regression remains.
+
+### Complete View correctness, lifecycle, and current parity
+
+- **Status and inspected surface:** Complete; implementation, focused validation, the authoritative gate, fresh self-review, and independent code review—including the compile-pass failure cleanup follow-up—are signed off. The audit covered every View source and test file; Foundation configuration and `view:cache`; Support facade metadata; Mail's Markdown consumer; Boost documentation; split metadata; current Laravel View source, tests, documentation, and full-app fixtures; and carried `view-01` and `reflection-02`. The detailed design is recorded in [`2026-08-06-0916-view-correctness-lifecycle-and-current-parity.md`](2026-08-06-0916-view-correctness-lifecycle-and-current-parity.md).
+
+| Findings | Final decision |
+|---|---|
+| `view-02` | Flush slot and slot-stack state so rendered slot content and `ComponentSlot` objects are not retained for the rest of the request. |
+| `view-08` | Flush loop frames so a failed render cannot corrupt loop depth and parent metadata in the next render. |
+| `view-03`, `view-05`, `view-06`, `view-14`, `view-21`, `view-22` | Correct style merging, conditional stacks, nested component resolution, end-directive signatures, enum component names, and unnamed slots. |
+| `view-04`, `view-26`, `view-27` | Publish exact inline-template contents atomically, bound each retained cache key's bytes, and replace the reserved namespace hint. |
+| `view-07`, `view-15`, `view-16`, `view-23`, `view-28`, `view-29`, `view-31`, `view-36` | Give transient compiler state exact compile-pass/coroutine ownership, restore extension contracts, isolate temporary echo formats, and complete strict/current maintenance. |
+| `view-09`, `view-17`, `view-18` | Publish canonical compiler defaults, restore provider extension visibility, and document worker-lived mutators. |
+| `view-10`, `view-11`, `view-19`, `view-20`, `view-32` | Correct split dependencies, remove dead validation middleware, document public differences and compiled-view freshness, and break the View→Foundation dependency cycle. |
+| `view-12`, `view-13`, `view-25` | Port bounded current maintenance, truthful named-argument/PHPDoc surfaces, and current full-app integration coverage. |
+| `view-24` | Deduplicate deployment-time cache roots by canonical directory boundary while preserving prefix siblings and filesystem roots. |
+| `view-30`, `view-35` | Memoize only verified-fresh compiled views and exhaustively release compiled-path ownership. |
+| `view-33`, `view-34` | Render mutable View section content before storage and retain Laravel's lazy yield-default behavior. |
+| `view-37` | Standardize `Component` and `CompilerEngine` static cleanup, complete the component cache reset, and keep the new worker-global cleanup method out of template scope. |
+| `view-38` | Remove `Component::ignoredParameterNames()` from template scope as internal metadata, completing the base Component method filter without claiming a separate runtime defect. |
+| `view-39` | Split top-level arguments across conditional stacks, once-only stacks, and `@json`, preserving nested expressions and HTML-safe JSON flags. |
+| `view-40` | Complete concise method metadata and the slot-context return type in touched View source. |
+| `view-41` | Clear raw-block and component-hash state after every compile pass while preserving public component-hash seeds for the immediately following pass. |
+
+- **Architecture and worker ownership:** Factory, BladeCompiler, EngineResolver, component metadata, compiled-template names, and verified freshness remain worker-lived. Render state and transient compiler state remain coroutine-local. Every compile pass initializes its current-section, footer, and protected raw-block owners, then clears raw blocks and component hashes on exit. Component hashes are not reset at entry because their public facade API may seed the immediately following pass. No compiler stack, snapshot, or clone was added. Inline publication stays lock-free at the existing atomic Filesystem replacement boundary.
+- **Correctness and parity:** Failed renders now clear loop frames before they can corrupt later loop metadata. Slot cleanup closes a retained-state leak—no later render reads stale slot state, but rendered slot HTML and `ComponentSlot` objects no longer remain in coroutine context for the rest of the request. Failed or incomplete compilations no longer retain raw template bytes or component hashes in the active context, and successful opening-only component compilations are bounded as well. Components resolve current nested, slot, enum, attribute, and directive forms. Inline templates recognize legitimate empty files and repair incomplete files. Compiled views honor disabled caching, recover after verified-file deletion, and always pop diagnostic path state. Top-level directive parsing preserves commas in conditions, stack names, nested expressions, and inline JSON arrays; `@json` no longer silently replaces its `JSON_HEX_*` defaults and weakens attribute-context escaping. Standard cleanup resets all Component and CompilerEngine static state without exposing the new Component mutator to templates. Provider visibility, named arguments, protected compiler contracts, facades, dependencies, and current integration scenarios are restored. `@elsePushIf`, exact inline publication, loop cleanup, directory-boundary roots, worker/coroutine echo ownership, render-before-store section content, exhaustive path cleanup, complete directive splitting, and compile-state failure cleanup intentionally correct behavior beyond current Laravel without removing a useful Laravel API.
+- **Important rejected concerns and closed limitations:** Do not add publication locks, retries, polling, file watchers, cache eviction, a section stack, compiler/Factory clones, request-scoped services, render-state snapshots, compile-state snapshots, placeholder registries, parsed-expression caches, directive grammar objects, or compatibility aliases. A component hash seeded through the public API must be consumed by the immediately following compile pass; arbitrary intervening passes are not preserved because doing so would retain stale stack state. Directive splitting uses one compile-time PHP-token pass whenever an expression contains a comma; comma counts cannot safely identify top-level separators. Class components still require their explicit alias before data, so no undocumented two-argument class grammar or synthesized alias was added. Dynamic inline templates still retain one fixed-size key and compiled file per distinct template; bounding cardinality would require unjustified eviction machinery. The opaque parent-placeholder salt is immutable worker state and needs no test reset because stored sections are already cleared. Compiled output deliberately names the base Factory, so overriding the protected salt on a Factory subclass is not a complete cached-template extension point. `@parent` inside a separately compiled include no longer depends on cold-cache ordering; that unsupported pattern consistently uses the include's empty compile-pass placeholder instead of freezing whichever outer section happened to compile first.
+- **Cross-package revalidation:** The complete View surface retains `view-01` request overlay precedence/restoration and revalidates `reflection-02` through closure inference and component/compiler reflection. Foundation owns `view-09` and `view-24`; its canonical config and command tests cover both. Testing's authoritative subscriber invokes View's standardized `view-37` cleanup hooks. Testbench's duplicate framework-reset registry is deleted; its raw PHPUnit base owns exception-handler restoration through the same protected seam and exhaustive failure ordering as the Components base. Foundation documents that caller-driven exception because it uniquely needs the active test case, and Testing covers both base classes' cleanup-failure path. Boost's reserved-keyword list matches the complete base Component filter. The later full Boost, Testbench, and Testing audits are indexed. View removes its false Foundation/Validation requirements while retaining optional Foundation directives through Composer `suggest`. The byte-identical Translation stringable boundary is separately owned as `translation-10`; View's sibling boundary is complete under `view-28` without editing the active Translation worktree.
+- **Upstream-ready defects:** Current Laravel shares the naive comma splitting in `@json`, conditional stacks, and once-only push/prepend directives. Multi-key inline JSON can silently lose the default `JSON_HEX_*` flags; the stack forms emit malformed argument boundaries for literal or nested commas. These sites are ready for an owner-coordinated upstream report; no external report is part of this branch.
+- **Implementation and cleanup:** Compiler, layout, component, engine, provider, Factory, finder, and package boundaries now use their final ownership model. The obsolete footer property, parent-placeholder context map/getter, validation middleware, duplicate defaults, stale dependencies, superseded compiled files through marker `v3`, and inaccurate docs/comments are removed. Facades are generated from the corrected concrete methods rather than edited by hand.
+- **Regression tests:** Counterfactual coverage proves slot/loop cleanup, exact inline publication—including the complete-file no-write branch—and bounded keys, nested/unnamed/enum components, conditional and once-only stack parsing with literal/nested commas and generated IDs, HTML-safe JSON options and depth, complete static resets and bare-component data keys, sequential and concurrent compile ownership, raw-block and component-hash cleanup after caught failures, successful opening-only hash cleanup, the public immediate seed-then-compile sequence, nested/failing/concurrent echo overrides including Mail, footer/end-directive contracts, lazy defaults, verified-fresh cache behavior, exhaustive compiled-path cleanup, provider/config/metadata/facade contracts, root-safe cache traversal, and current full-app View behavior. The protected slot-context assertion pins retained-state cleanup because no output path rereads stale slots. The Xdebug-only `ParseError` branches are contained at their source boundaries without adding a synthetic test seam; ordinary incomplete expressions cover the trailing-token guard.
+- **Performance and complexity:** The component path adds one xxh128 digest per uncached lookup and filesystem checks only before the existing static name cache is populated. Compiler isolation adds constant-time context operations only at compilation boundaries; compile-state initialization and cleanup each use one batched context resolution. Echo overrides add one context read per escaped echo during compilation, not rendering. Comma-free affected directives return immediately; comma-bearing directives pay one bounded `token_get_all()` pass during template compilation only, with no render-time work. Parent placeholders use a fast digest instead of retaining a per-section map. Fresh compiled views retain their one-check-per-worker fast path; only the render immediately after a stale compile performs one additional freshness check next time. Cache-root work is deployment-only. Static cleanup and metadata changes run only at explicit reset/test boundaries. No request path gains a lock, retry, yield, poll, watcher, container loop, network call, serialization layer, or unbounded new state.
+- **Laravel-facing result:** Supported View and Blade method names, named arguments, provider extension points, protected compiler hooks, component syntax, facades, configuration, and render behavior are compatible or restored to current Laravel. `newComponentHash()` remains facade-exposed and supports Laravel's immediate seed-then-compile sequence; a seed no longer survives an unrelated intervening compile pass. Comma-bearing supported directives now compile by top-level arguments, and multi-key inline `@json` arrays retain HTML-safe default flags. Alias-first `Blade::component()` and render-before-store View section content remain the two documented public Hypervel differences. The removed `getParentPlaceholder()` was a superseded Hypervel divergence, not a current Laravel API.
+- **Validation and review:** Changed test files and the combined View, Integration/View, Foundation View, Mail View, facade, and metadata coverage are green. Facades and split metadata have been regenerated and checked. The prior authoritative `composer fix` gate passed formatting, both PHPStan configurations, the complete parallel suite, Testbench package mode, and dogfood. The compile-state follow-up passes the complete View group, both PHPStan configurations, targeted formatting, and `git diff --check`; independent follow-up review verified the final tree and signed off with no remaining finding.
+- **Assessment:** Every accepted View finding is implemented at its owning boundary without a local workaround, speculative abstraction, stale compatibility path, meaningful hot-path regression, or unintended Laravel API removal. No View TODO, deferred defect, or open workflow step remains.
diff --git a/docs/plans/2026-08-06-0916-view-correctness-lifecycle-and-current-parity.md b/docs/plans/2026-08-06-0916-view-correctness-lifecycle-and-current-parity.md
new file mode 100644
index 000000000..f9f5d03df
--- /dev/null
+++ b/docs/plans/2026-08-06-0916-view-correctness-lifecycle-and-current-parity.md
@@ -0,0 +1,611 @@
+# View Correctness, Lifecycle, and Current Parity
+
+## Status and scope
+
+**Status:** Complete; follow-up implementation, validation, self-review, and code review signed off.
+
+Complete the View audit against:
+
+- Hypervel `0.4` at this branch's base;
+- current Laravel framework source, View tests, and full-app View integration fixtures under `examples/laravel/framework`;
+- the completed `view-01` request-shared-data and `reflection-02` callable-shape decisions;
+- current View, Foundation configuration and console commands, Support facade metadata, Boost documentation, split-package metadata, Mail factory clones, and framework test-state cleanup.
+
+This is a correction and parity pass, not a redesign. Preserve Hypervel's worker-singleton Factory/compiler architecture, coroutine-local render and compile state, worker-lived immutable metadata and freshness caches, lock-free component creation, alias-first `Blade::component()` registration, strict string compiler paths, and request-shared-data overlay. No accepted change adds a lock, watcher, retry, registry, request-scoped Factory/compiler, render-state snapshot, eviction policy, or compatibility shim.
+
+No useful Laravel API is removed. The public component-hash seed path remains supported when the immediately following compile pass consumes it; retaining a seed across unrelated intervening passes is removed to bound stale state. The restored provider, compiler, layout, dynamic-component, named-argument, and PHPDoc surfaces improve parity. The intentional alias-first API and strict path model remain documented Hypervel differences. The `@elsePushIf` repair and failed-render loop cleanup correct defects that current Laravel also carries.
+
+### Approved tradeoffs
+
+| Findings | Benefit | Cost | Rejected machinery |
+|---|---|---|---|
+| `view-04`, `view-26`, `view-27` | Publish complete inline templates, bound retained key bytes, and keep the reserved namespace bounded. | One xxh128 digest per cache lookup; filesystem replacement only on first/incomplete publication. | Locks, retries, polling, a publication registry, LRU/TTL, or a second render path. |
+| `view-07`, `view-08`, `view-29`, `view-33`, `view-35` | Isolate transient compiler/render state and clean it deterministically. | Constant-time coroutine-context reads/writes at existing compile, render, or cleanup boundaries. | Compiler/Factory cloning, request-scoped services, or full-state transactions. |
+| `view-24` | Avoid duplicate deployment-time compilation below nested roots. | A bounded in-memory path comparison in `view:cache`; no request-path cost. | Persistent path indexes or filesystem watchers. |
+| `view-25` | Prove provider/facade/full-app interactions currently absent from package tests. | Test-only fixtures and execution. | A View-specific harness or one oversized integration test. |
+
+## Post-compaction and design rules
+
+After compaction, re-read `AGENTS.md` and this plan in full before editing. Re-open the active source and tests; summaries are navigation only.
+
+## What this audit is not
+
+This audit is not permission to add defensive machinery for every imaginable failure. Do not add an abstraction, state machine, retry loop, configurable timeout, registry, mutex, context slot, cache, or compatibility API merely because it sounds robust.
+
+Complexity must pay for itself with at least one of:
+
+- a demonstrated failure;
+- a complete source trace proving a realistic vulnerable schedule;
+- a clear general capability with real consumers and owner approval;
+- deletion of greater or riskier complexity elsewhere.
+
+Typical Laravel lifecycle semantics define the supported contract. A package that intentionally relies on model events, middleware, listeners, transactions, or another documented mechanism is not defective merely because userland can explicitly bypass that mechanism. Do not build a parallel enforcement path for `withoutEvents()`, raw database writes, disabled middleware, direct transport access, or comparable deliberate bypasses unless the public contract explicitly promises behavior through that bypass.
+
+Underengineering is equally a failure. Fix every verified defect completely at its lowest owning boundary, never with a partial fix or a local patch over a broken shared contract, and always surface meaningful evidence-backed improvements rather than dropping them to avoid effort. Restraint applies to speculative machinery and cosmetic change, not to complete fixes or worthwhile opportunities.
+
+Do not treat an upstream difference as a bug without tracing it. Do not treat upstream parity as proof of correctness. A real Hypervel defect remains a defect when Laravel, Hyperf, Symfony, or an SDK has the same hole.
+
+The audit categories are discovery lenses, not boundaries around what may be corrected. Any genuine issue discovered while auditing, implementing, testing, or reviewing must be investigated, assigned to its lowest owning boundary, and taken through the applicable consensus, implementation, validation, review, and approval workflow—even when it is outside the current package, initial taxonomy, or changed diff. Do not dismiss a verified issue as unrelated or defer it merely to preserve package order. This rule applies only after the evidence threshold is met; it does not turn speculative concerns, deliberate bypasses, unsupported use, or contract violations into work.
+
+## Audit principles
+
+### 1. Verify before changing
+
+A suspicious pattern is not an actionable finding until the audit establishes:
+
+- the exact file and symbol;
+- every relevant caller and callee across `src/` and `tests/`;
+- the state or resource owner;
+- the initialization, commit, use, and cleanup boundaries;
+- a realistic production or test failure schedule;
+- why current guards and tests do not prevent it;
+- sibling implementations and same-family sites;
+- relevant upstream behavior;
+- the lowest correct fix boundary;
+- a regression strategy;
+- the performance and complexity effect of the proposed fix.
+
+Use a focused probe when source reasoning cannot settle native or scheduler behavior. Do not repeatedly run the full suite hoping to reproduce a rare flake.
+
+### 2. Fix the lowest inconsistent contract
+
+Do not add local compensation when a shared lower-level contract is wrong. A caller catch is not enough when a typed filesystem method can return `false`; a per-consumer spawn catch is not enough when Engine exposes an ambiguous spawn contract; a proxy workaround is not enough when pool ownership is undefined.
+
+After changing a lower-level contract, re-audit every affected caller and revisit completed packages that depend on it. Record cross-references in both the owning package and each affected package ledger entry.
+
+### 3. Make ownership explicit
+
+The component that acquires or registers a resource records the exact handle and releases that exact handle. Cleanup must not reconstruct identity from mutable state when the original handle can be retained.
+
+Examples include coroutine IDs, timer IDs, process IDs plus incarnation checks, listener callbacks, pool leases, subscriber objects, stream handles, temporary filenames, signal watcher IDs, and channel tokens.
+
+### 4. Make creation transactional
+
+If code reserves capacity or publishes state before a later operation can fail, it must either finish creation or roll back every earlier change. Do not expose half-initialized objects, registered-but-dead pools, leaked wait-group counts, or published runtime paths without their cleanup owner.
+
+### 5. Make cleanup exhaustive
+
+Independent cleanup steps run even when an earlier step fails. The earliest operation or cleanup failure remains primary. Cleanup failures must not corrupt bookkeeping, skip unrelated cleanup, or turn a successful ownership transfer into a reported failure.
+
+### 6. Bound only external progress
+
+Use deadlines where progress depends on a process, socket peer, lock owner, IPC child, or external service that can disappear. Do not add arbitrary timeouts to ordinary internal coroutine joins once successful creation and ownership guarantee completion.
+
+### 7. Preserve hot-path quality
+
+For every fix, inspect:
+
+- additional allocations;
+- container or facade resolutions;
+- locking and atomics;
+- hashing and serialization;
+- new yields or sleeps;
+- retries and polling;
+- logging or exception construction;
+- retained worker memory;
+- cache invalidation and eviction.
+
+A correctness guard on a cold failure path has a different cost from a new lock or resolver on every request. State the difference explicitly.
+
+Any proposed change with a measured or source-proven hot-path regression requires explicit owner approval before implementation, even when it fixes a defect. Present the expected frequency and magnitude, the evidence, and the viable alternatives. Do not hide an unavoidable tradeoff inside a general correctness claim.
+
+Performance improvements must provide a meaningful practical benefit after accounting for code complexity and divergence from upstream. Measure representative behavior where practical. Always surface an evidence-backed opportunity to the owner, but do not implement it without approval; a micro-optimization within measurement noise is neither a reason to diverge nor an actionable finding.
+
+### 8. Remove superseded design completely
+
+When a fix changes the owning model, delete obsolete helpers, callbacks, properties, config keys, comments, tests, and documentation. Do not leave a compatibility path or comment describing behavior that no longer exists. Preserve intentional upstream comments unless the new design makes them incorrect.
+
+### 9. Treat remediation patterns as candidates
+
+The established patterns later in this plan are a vocabulary, not a lookup table. Choose among per-call parameters, immutable values, scoped bindings, cloning, CoroutineContext, factories, explicit ownership, static reset, or resource teardown only after proving the real lifetime and owner.
+
+### 10. Reject speculative complexity
+
+Record low-confidence concerns under rejected or unresolved analysis. Do not implement them. Surface every evidence-backed, meaningful non-defect improvement to the owner with its benefit, cost, and alternatives, then stop for explicit approval. This requirement exists to keep worthwhile opportunities visible, not to discourage finding them.
+
+## Research and final decisions
+
+### State and ownership
+
+`ViewServiceProvider` creates worker-singleton Factory, Blade compiler, and EngineResolver instances. Boot configuration, resolved engines, finder hints, component metadata, inline-template names, and verified compiled-view freshness are intentionally worker-lived. Sections, stacks, components, slots, loops, fragments, translations, compiled paths, and transient compiler state are coroutine-local. Component instances remain fresh per render through `buildWith()`.
+
+`Coroutine::fork()` copies ordinary objects by reference. A `View` stored as section content must therefore render before entering `CoroutineContext`; a default passed to `yieldContent()` is only a local value and must render only when the section is absent.
+
+The package owns filesystem publication and output-buffer cleanup, but no persistent socket, process, timer, channel, or lease. Filesystem replacement is the existing transaction boundary for inline templates. `PhpEngine` unwinds output buffers and formats exceptions before `CompilerEngine::get()` returns, so a surrounding `finally` can always pop the compiled path without losing diagnostics.
+
+### Upstream and compatibility
+
+The accepted maintenance set was checked against current Laravel source, unit tests, `tests/Integration/View`, and the originating changes listed in the audit evidence. Preserve Hypervel's worker-lived freshness/component metadata, coroutine-local request-shared-data overlay (`view-01`), alias-first component registration, and strict string compiler paths. The intentional implementation divergences relevant to this work are exact-size inline publication (`view-04`), corrected conditional-stack parsing (`view-05`), loop cleanup (`view-08`), direct placeholder hashing without Laravel's memo map (`view-15`), directory-boundary cache-root comparison (`view-24`), reserved-namespace replacement (`view-27`), split worker/coroutine echo formats (`view-29`), verified-fresh-only memoization (`view-30`), render-before-store section content (`view-33`), exhaustive compiled-path cleanup (`view-35`), strict loop/token identity (`view-36`), standardized static cleanup (`view-37`), internal component-method filtering (`view-38`), and top-level directive argument parsing (`view-39`). `view-34` removes a divergence by restoring Laravel's lazy default rendering. These are internal correctness or lifecycle differences and do not remove a useful Laravel-facing API.
+
+The inherited compiled Factory FQCN means overriding `parentPlaceholderSalt()` on a Factory subclass is not complete for cached `@parent` output. Do not claim otherwise and do not invent a dynamic compiler indirection in this work.
+
+`stringable()` accepts only the two registration shapes its string-keyed handler map can represent: a class string plus handler, or a typed closure from which the class is inferred. Other callable shapes cannot be array keys, so the corrected `Closure|string` type does not remove working behavior.
+
+### Findings
+
+| ID | Result |
+|---|---|
+| `view-01` | Revalidate the completed coroutine-local request-data overlay; no source change. |
+| `view-02` | Flush slot and slot-stack state with component state. |
+| `view-03` | Terminate a nonempty default `style` before concatenation. |
+| `view-04` | Publish missing, empty, or partial inline templates with `Filesystem::replace()`. |
+| `view-05` | Parse comma-bearing conditions in `@pushIf` and `@elsePushIf`; superseded parsing is consolidated by `view-39`. |
+| `view-06` | Resolve `Namespace\Accordion\Accordion` for nested default components. |
+| `view-07` | Scope the compiler's current section name to one coroutine and compile pass, and define standalone `@parent` as the empty-section placeholder. |
+| `view-08` | Flush loop frames through `Factory::flushState()`. |
+| `view-09` | Put the four shipped compiler defaults in Foundation's View config and remove duplicate provider fallbacks. |
+| `view-10` | Declare direct Symfony dependencies and View package provenance. |
+| `view-11` | Delete dead `ValidationExceptionHandle` and remove the unused Validation dependency. |
+| `view-12` | Port the bounded current-Laravel readability/reflection maintenance and missing loop coverage. |
+| `view-13` | Restore named-argument and PHPDoc accuracy. |
+| `view-14` | Accept `?string $expression` on the three protected end-directive compilers. |
+| `view-15` | Restore static `parentPlaceholder()` and protected static salt; remove `getParentPlaceholder()`, context map, and stale compiled output via marker `v3`. |
+| `view-16` | Restore one-argument protected `addFooters()` using context-owned footer state. |
+| `view-17` | Restore public visibility on seven provider registration methods. |
+| `view-18` | Document worker-lived mutators at their concrete boundaries. |
+| `view-19` | Record the approved alias-first component API as a Laravel difference. |
+| `view-20` | Document first-use-per-worker compiled-view freshness accurately. |
+| `view-21` | Accept `BackedEnum|string` dynamic component names. |
+| `view-22` | Default unnamed slots to `slot`. |
+| `view-23` | Catch Xdebug-originated `ParseError` in `hasEvenNumberOfParentheses()`. |
+| `view-24` | Deduplicate nested `view:cache` roots without conflating path-prefix siblings. |
+| `view-25` | Port the current full-app View integration surface and fixtures. |
+| `view-26` | Hash inline-template cache keys to bound retained bytes. |
+| `view-27` | Replace, rather than append, the reserved `__components` namespace. |
+| `view-28` | Narrow `stringable()` to its actually supported `Closure|string` domain; route the Translation twin separately. |
+| `view-29` | Make boot echo format worker-lived and callback overrides coroutine-local and nest-safe. |
+| `view-30` | Memoize only verified-fresh compiled paths, restoring `view.cache=false` and first-render deletion recovery. |
+| `view-31` | Delete the superseded footer property and type/relocate `pushFooter()`. |
+| `view-32` | Remove the View→Foundation cycle; suggest Foundation for optional Vite/fonts directives. |
+| `view-33` | Preserve and document render-before-store for View-valued section content copied across coroutine contexts. |
+| `view-34` | Do not render a default View when `yieldContent()` finds an existing section. |
+| `view-35` | Pop the compiled-path stack in `finally` across success and failure. |
+| `view-36` | Use strict identity for the six loose loop/token comparisons in View source. |
+| `view-37` | Give `Component` and `CompilerEngine` complete standardized static cleanup hooks without exposing the new worker-global mutator to templates. |
+| `view-38` | Remove `Component::ignoredParameterNames()` from template scope as internal metadata. |
+| `view-39` | Split top-level arguments correctly across conditional stacks, once-only stacks, and `@json`, preserving HTML-safe JSON flags. |
+| `view-40` | Complete concise method metadata and the slot-context return type in touched View source. |
+| `view-41` | Clear raw-block and component-hash compile state after success or failure while preserving the public immediate seed-then-compile component contract. |
+
+## Implementation design
+
+### 1. Complete render-state cleanup (`view-02`, `view-08`)
+
+Keep cleanup with the traits that own the context keys:
+
+```php
+protected function flushComponents(): void
+{
+ CoroutineContext::set(static::COMPONENT_STACK_CONTEXT_KEY, []);
+ CoroutineContext::set(static::COMPONENT_DATA_CONTEXT_KEY, []);
+ CoroutineContext::set(static::CURRENT_COMPONENT_DATA_CONTEXT_KEY, []);
+ CoroutineContext::set(static::SLOTS_CONTEXT_KEY, []);
+ CoroutineContext::set(static::SLOT_STACK_CONTEXT_KEY, []);
+}
+
+protected function flushLoops(): void
+{
+ CoroutineContext::set(static::LOOPS_STACK_CONTEXT_KEY, []);
+}
+```
+
+Call `flushLoops()` from the existing `Factory::flushState()` sequence. Add one real failed-render regression for loop cleanup and the upstream slot reset regression. Do not snapshot state around every render.
+
+Loop cleanup prevents stale depth and parent metadata from affecting a later render. Slot cleanup instead bounds retained request memory: every component creation overwrites its readable slot entry, but without the flush, rendered slot HTML and `ComponentSlot` objects remain in coroutine context for the rest of the request.
+
+### 2. Correct attributes, directives, component resolution, and public types (`view-03`, `view-05`, `view-06`, `view-13`, `view-14`, `view-21`, `view-22`)
+
+Normalize both sides of a style merge without changing non-string appendable defaults:
+
+```php
+if ($key === 'style') {
+ $value = Str::finish($value, ';');
+
+ if (is_string($defaultsValue) && $defaultsValue !== '') {
+ $defaultsValue = Str::finish($defaultsValue, ';');
+ }
+}
+```
+
+Split only top-level directive arguments in the shared compiler. A no-comma expression returns immediately; comma-bearing expressions use PHP's tokenizer so strings and nested `()`, `[]`, and `{}` remain intact:
+
+```php
+protected function splitTopLevel(string $expression, int $limit): array
+{
+ $expression = $this->stripParentheses($expression);
+
+ if (! str_contains($expression, ',')) {
+ return [$expression];
+ }
+
+ try {
+ $tokens = token_get_all('stripQuotes(
+ $matches['inlineName'] ?: $matches['name'] ?: $matches['boundName']
+) ?: "'slot'";
+
+public string $component;
+
+public function __construct(BackedEnum|string $component)
+{
+ $this->component = (string) enum_value($component);
+}
+```
+
+Rename `FileViewFinder::find()` to `$view` and View's ArrayAccess parameters to `$offset`; add the conditional `ComponentAttributeBag::data()` return PHPDoc and `ComponentSlot::hasActualContent()` exception PHPDoc. Give `compileEndsession()`, `compileEnderror()`, and `compileEndcontext()` the nullable expression argument their dispatcher already supplies. Add direct output, reflection, and subclass compatibility tests.
+
+### 3. Make inline component templates bounded and transactional (`view-04`, `view-26`, `view-27`)
+
+Use a fixed-size cache key and keep cache cardinality/values unchanged:
+
+```php
+$key = hash('xxh128', sprintf('%s::%s', static::class, $contents));
+```
+
+At publication, replace the reserved hint and accept a file only when its exact size matches the content:
+
+```php
+$container = Container::getInstance();
+$files = $container->make(Filesystem::class);
+$directory = $container->make('config')->string('view.compiled');
+$viewFile = $directory . '/' . hash('xxh128', $contents) . '.blade.php';
+
+$factory->replaceNamespace('__components', $directory);
+
+if (! $files->exists($viewFile) || $files->size($viewFile) !== strlen($contents)) {
+ $files->ensureDirectoryExists($directory);
+ $files->replace($viewFile, $contents);
+}
+```
+
+This recognizes legitimate empty templates and repairs zero-byte or partial artifacts. Cover an empty template, a truncated template, atomic publication, fixed-size keys without raw contents, repeated reserved-namespace replacement, and normal cache hits. Do not add locks or eviction.
+
+### 4. Isolate compile-pass state and restore compiler extension contracts (`view-07`, `view-12`, `view-16`, `view-23`, `view-28`, `view-29`, `view-31`, `view-36`)
+
+Declare `protected const LAST_SECTION_CONTEXT_KEY = '__view.last_section';` on `CompilesLayouts`, which exclusively owns the transient section state like the other compiler concerns own their context keys. Replace `$lastSection` with that context entry and define a standalone `@parent` as the empty-section placeholder instead of an incidental `TypeError`:
+
+```php
+CoroutineContext::set(static::LAST_SECTION_CONTEXT_KEY, '');
+
+CoroutineContext::set(static::LAST_SECTION_CONTEXT_KEY, trim($expression, "()'\" "));
+
+$lastSection = CoroutineContext::get(static::LAST_SECTION_CONTEXT_KEY, '');
+$escapedLastSection = strtr($lastSection, ['\\' => '\\\\', "'" => "\\'"]);
+```
+
+Initialize the key at the start of every `compileString()` call, beside the footer reset. This prevents one sequential compile, including a compile on another compiler instance in the same coroutine, from supplying the section for a later standalone `@parent`. Prove deterministic interleaving with a yielding custom directive and strengthen the standalone regression by compiling a named section first on the same compiler. Preserve the existing single-slot compile-pass semantics within one coroutine; this finding isolates sibling compilations and compile-pass ownership rather than adding a reentrant compiler stack. No lock, instance discriminator, compiler clone, or reset hook is permitted.
+
+An `@parent` inside an included partial previously depended on whichever section happened to be current when that partial was first compiled. Precompilation or a different cache order could therefore freeze a different placeholder. This unsupported, cache-order-dependent pattern is not preserved with extra state machinery; each separately compiled template owns its own section context.
+
+Restore footer ownership and delete the now-dead property:
+
+```php
+protected function addFooters(string $result): string
+{
+ $footers = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
+
+ return ltrim($result, "\n") . "\n" . implode("\n", array_reverse($footers));
+}
+
+/**
+ * Push a footer onto the stack.
+ */
+protected function pushFooter(string $footer): void
+{
+ $footers = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
+ $footers[] = $footer;
+ CoroutineContext::set(static::FOOTER_CONTEXT_KEY, $footers);
+}
+```
+
+Place `pushFooter()` immediately after `addFooters()` so the ownership stays together.
+
+Update `BladeCompiler::compileString()`'s existing guarded call site to use the restored one-argument contract while retaining the local footer guard:
+
+```php
+$footers = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
+
+if (count($footers) > 0) {
+ $result = $this->addFooters($result);
+}
+```
+
+The echo format has a worker default and optional coroutine override:
+
+```php
+protected string $echoFormat = 'e(%s)';
+
+public function setEchoFormat(string $format): void
+{
+ $this->echoFormat = $format;
+}
+
+protected function getEchoFormat(): string
+{
+ return CoroutineContext::get(static::ECHO_FORMAT_CONTEXT_KEY, $this->echoFormat);
+}
+
+public function usingEchoFormat(string $format, callable $callback): string
+{
+ $hadOverride = CoroutineContext::has(static::ECHO_FORMAT_CONTEXT_KEY);
+ $previous = CoroutineContext::get(static::ECHO_FORMAT_CONTEXT_KEY);
+
+ CoroutineContext::set(static::ECHO_FORMAT_CONTEXT_KEY, $format);
+
+ try {
+ return call_user_func($callback);
+ } finally {
+ if ($hadOverride) {
+ CoroutineContext::set(static::ECHO_FORMAT_CONTEXT_KEY, $previous);
+ } else {
+ CoroutineContext::forget(static::ECHO_FORMAT_CONTEXT_KEY);
+ }
+ }
+}
+```
+
+`usingEchoFormat()` must not call `setEchoFormat()`: it owns only the temporary coroutine override. `withDoubleEncoding()` and `withoutDoubleEncoding()` update the worker default through `setEchoFormat()`. The only format read is in `CompilesEchos`, so this adds one context lookup per escaped echo at compile time, not per render. Mail's `Markdown` is the only external consumer and requires the override across nested compilation. `$echoFormat` needs no static cleanup because the test container owns and discards the singleton instance. Add boot-outside/request-inside, nested, exceptional, Mail nesting, and deterministic sibling-coroutine tests.
+
+Narrow `stringable()` to its implementable contract:
+
+```php
+public function stringable(Closure|string $class, ?callable $handler = null): void
+```
+
+Keep the two supported registration forms and regenerate facade metadata from the corrected concrete method; do not hand-edit generated signatures. Revalidate and route the byte-identical Translation issue as `translation-10`; do not edit the concurrent Translation worktree from this branch.
+
+Port the bounded upstream maintenance in place: direct empty-array checks, explicit `implode('', ...)`, direct `Stringable` use, current component reflection/filtering, alias derivation, and the two missing uncountable-loop tests. Make the complete loose-comparison inventory strict: `ManagesLoops` uses `===` for initial `last`, incremented `first`, and incremented `last`; `BladeCompiler::parseToken()` uses `=== T_INLINE_HTML`; and both parenthesis-token comparisons use `===`. Catch only `ParseError` around `token_get_all()` with a concise WHY naming Xdebug. Do not add a synthetic runtime seam.
+
+Raw blocks are wholly protected compile-pass state, so initialize their context entry at `compileString()` entry. Component hashes differ: `newComponentHash()` and `compileEndComponentClass()` are public and facade-exposed, and Laravel's tests seed a hash immediately before the consuming compile pass. Preserve a hash present at entry, then clear both stores in `finally` after success or failure:
+
+```php
+CoroutineContext::setMany([
+ static::LAST_SECTION_CONTEXT_KEY => '',
+ static::FOOTER_CONTEXT_KEY => [],
+ static::RAW_BLOCKS_CONTEXT_KEY => [],
+]);
+
+try {
+ // Compile the complete template.
+} finally {
+ CoroutineContext::setMany([
+ static::RAW_BLOCKS_CONTEXT_KEY => [],
+ static::COMPONENT_HASH_STACK_CONTEXT_KEY => [],
+ ]);
+}
+```
+
+A public hash seed therefore belongs to the immediately following compile pass; unrelated intervening passes do not preserve it. Do not snapshot or restore arbitrary stack contents: well-formed class component tags open and close in one pass, and retaining pass state would preserve the verified worker-memory leak. Keep `restoreRawContent()` unchanged and retain the fail-fast typed empty-pop behavior.
+
+### 5. Restore the parent-placeholder and section-content model (`view-15`, `view-33`, `view-34`)
+
+Use one immutable worker salt and no section map:
+
+```php
+protected static ?string $parentPlaceholderSalt = null;
+
+public static function parentPlaceholder(string $section = ''): string
+{
+ // Deliberately not memoized: this is a pure function of one immutable salt.
+ return '##parent-placeholder-' . hash('xxh128', static::parentPlaceholderSalt() . $section) . '##';
+}
+
+protected static function parentPlaceholderSalt(): string
+{
+ return static::$parentPlaceholderSalt ??= Str::random(40);
+}
+```
+
+Remove `PARENT_PLACEHOLDER_CONTEXT_KEY` and `getParentPlaceholder()`. Use `static::parentPlaceholder()` in the layout trait and emit `\Hypervel\View\Factory::parentPlaceholder(...)` from compiled output. Regenerate the View facade and update tests. Bump the Compiler marker from `v2` to `v3` so existing compiled files cannot call the removed method, and update every marker-dependent expectation in `ViewBladeCompilerTest`; add no alias or manual-clear instructions. The salt needs no reset because it is opaque, immutable worker configuration and all stored sections are already reset.
+
+Keep the existing eager conversion only where a mutable View becomes stored section content, and add a concise WHY at that ownership boundary:
+
+```php
+$this->extendSection($section, $content instanceof View ? $content->render() : e($content));
+```
+
+Avoid rendering an unused yield default:
+
+```php
+$sections = CoroutineContext::get(static::SECTIONS_CONTEXT_KEY, []);
+$sectionContent = isset($sections[$section])
+ ? $sections[$section]
+ : ($default instanceof View ? $default->render() : e($default));
+```
+
+Keep `isset` semantics. Prove that stored section content does not share a mutable View across copied contexts, an absent default still renders, and a present section does not render the default or produce its stack side effects. Document only the section-storage timing difference.
+
+### 6. Preserve compiled-view cache semantics and diagnostics (`view-30`, `view-35`)
+
+Memoize only a path verified fresh before evaluation:
+
+```php
+if (! isset(static::$compiledOrNotExpired[$path])) {
+ if ($this->compiler->isExpired($path)) {
+ $this->compiler->compile($path);
+ } else {
+ static::$compiledOrNotExpired[$path] = true;
+ }
+}
+```
+
+Remove the unconditional memo after evaluation. This makes `view.cache=false` compile every time, retains the one-check-per-worker hot path for fresh/precompiled views, pays one extra freshness check only after a first stale compile, and lets the existing missing-file recovery handle deletion after the first fresh check.
+
+Wrap the entire path ownership in `finally`:
+
+```php
+$this->pushCompiledPath($path);
+
+try {
+ // freshness, evaluation, and missing-file recovery
+ return $results;
+} finally {
+ $this->popCompiledPath();
+}
+```
+
+Add cache-enabled hot-path, cache-disabled repeated compilation, first-render deletion recovery, successful stack cleanup, and caught-failure stack cleanup tests. Keep exception formatting inside `PhpEngine`, where the current path remains visible before `finally` runs.
+
+### 6a. Standardize static cleanup and component method filtering (`view-37`, `view-38`)
+
+Keep Laravel's specific reset APIs and compose them through Hypervel's standard test hook:
+
+```php
+public static function flushState(): void
+{
+ static::flushCache();
+ static::forgetFactory();
+ static::forgetComponentsResolver();
+}
+```
+
+`Component::flushCache()` also resets `$ignoredParameterNames`, which is a reflection cache and cannot become stale within a worker. `CompilerEngine::flushState()` delegates to `forgetCompiledOrNotExpired()`, which remains the production API used by `ViewClearCommand`. The test subscriber calls only the two standardized hooks. Delete Testbench's duplicate framework-reset registry. Its raw PHPUnit base restores exception-handler state through the same protected seam and exhaustive teardown ordering as the Components test base, because restoring PHPUnit's handler uniquely requires the active test case. Put that ownership explanation once in `HandleExceptions::flushState()`, not at individual call sites. Add `flushState` to `Component::ignoredMethods()` so template evaluation cannot invoke the new worker-global cleanup method. Also exclude `ignoredParameterNames` because framework metadata does not belong in template scope; this is completeness with no separate reachable failure. Keep Boost's reserved-keyword list synchronized with the complete base Component filter.
+
+### 7. Make configuration and provider contracts explicit (`view-09`, `view-17`, `view-18`)
+
+Add the canonical Foundation defaults:
+
+```php
+'relative_hash' => false,
+'cache' => true,
+'compiled_extension' => 'php',
+'check_cache_timestamps' => true,
+```
+
+Read them without duplicate fallbacks in `ViewServiceProvider`. Make `registerFactory()`, `registerViewFinder()`, `registerBladeCompiler()`, `registerEngineResolver()`, `registerFileEngine()`, `registerPhpEngine()`, and `registerBladeEngine()` public; keep `createFactory()` protected.
+
+Add concise boot/test lifecycle warnings to `EngineResolver::register()`/`forget()` and `BladeCompiler::withoutComponentTags()`, `stringable()`, `setEchoFormat()`, `withDoubleEncoding()`, and `withoutDoubleEncoding()`. Do not warn on the `Factory::getFinder()` getter and do not make boot registries coroutine-local.
+
+### 8. Correct package boundaries and public documentation (`view-10`, `view-11`, `view-19`, `view-20`, `view-32`)
+
+In `src/view/composer.json`:
+
+```json
+"require": {
+ "symfony/http-foundation": "^8.1",
+ "symfony/http-kernel": "^8.1"
+},
+"suggest": {
+ "hypervel/foundation": "Required for the @vite, @viteReactRefresh, and @fonts directives."
+}
+```
+
+Remove `hypervel/foundation` and `hypervel/validation` from `require`. View references `Vite::class` only to emit generated code; Foundation remains an optional integration, matching Laravel's split boundary. Delete only `Middleware/ValidationExceptionHandle.php`; retain `ShareErrorsFromSession`, which Foundation registers.
+
+Add a View package metadata test pinning both Symfony constraints, both deliberate Hypervel omissions, and View provider discovery in root/split manifests. Keep the README minimal and in the prescribed order: package header and badge; `Documentation: https://hypervel.org/docs/views`; `Differences From Laravel` for alias-first registration and eager rendering only when a View becomes stored section content; then `Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/View`. Record optional Foundation integration only through Composer's `suggest` metadata, not as a public difference. Update Boost View docs to state that compiled freshness is checked on first use per worker, `view:cache` belongs in deployment, and `view:clear` is needed after local edits with live workers. Use Laravel-style user prose and do not expose internal context/cache implementation beyond what users need.
+
+### 9. Deduplicate cache roots correctly and port integration coverage (`view-24`, `view-25`)
+
+Canonicalize roots where possible, remove trailing separators before deduplication while preserving filesystem roots, and reject only a true descendant with a directory boundary:
+
+```php
+$paths = $paths
+ ->map(function (string $path): string {
+ $path = realpath($path) ?: $path;
+
+ return dirname($path) === $path ? $path : rtrim($path, DIRECTORY_SEPARATOR);
+ })
+ ->unique();
+
+return $paths->reject(function (string $path) use ($paths): bool {
+ // Trimming before appending preserves one boundary separator for filesystem roots.
+ $boundary = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+
+ return $paths->contains(
+ fn (string $existing): bool => $existing !== $path
+ && str_starts_with(
+ $boundary,
+ rtrim($existing, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
+ )
+ );
+})->values();
+```
+
+Preserve siblings such as `/views` and `/views-admin`, collapse trailing-separator duplicates even when `realpath()` fails, and retain `/` itself. Port Laravel's current `BladeTest`, `BladeAnonymousComponentTest`, `RenderableViewExceptionTest`, and fixtures into package-scoped Hypervel tests. Keep the classes separate. Rename the upstream `tested_...` method only for clear naming/TestDox; PHPUnit 13 already executes it, so do not claim the rename enables coverage. Adapt only framework bootstrap/namespaces and intentional Hypervel differences.
+
+## Testing and validation
+
+### Focused tests
+
+- `tests/View/ViewFactoryTest.php`: slot/slot-stack and loop cleanup, placeholder parity, stored View isolation, and unused yield-default side effects. Use a closure bound to the Factory only to prove both protected slot context entries are empty after `flushState()`; no public behavior exposes the retained slot-stack state.
+- `tests/View/ViewComponentAttributeBagTest.php`: default/current style termination and PHPDoc-preserving behavior.
+- `tests/View/ComponentTest.php`: digest keys, empty/partial publication recovery, namespace replacement, and cache hits. Protected cache inspection is explicitly allowed here because bounded retained key bytes are the load-bearing `view-26` invariant and no public output exposes the cache key.
+- `tests/View/Blade/BladePushTest.php` and `BladePrependTest.php`: top-level conditional and once-only stack arguments, including literal and nested commas, tokenizer-combined interpolation openers, and generated IDs.
+- `tests/View/Blade/BladeJsonTest.php`: multi-key inline arrays and interpolated strings retain default HTML-safe flags and depth.
+- `tests/View/Blade/BladeComponentTagCompilerTest.php`: nested custom namespace, unnamed slot, and backed-enum paths where owned.
+- `tests/View/ViewBladeCompilerTest.php` and focused Blade suites: compiler marker `v3`, coroutine section isolation, echo-format ownership, footer signature, end-directive overrides, Xdebug-only `ParseError` source behavior without a synthetic seam, supported stringable forms, strict maintenance, and loop cases.
+- `tests/View/ViewBladeCompilerTest.php` and `tests/View/Blade/BladeComponentsTest.php`: clear raw blocks and component hashes after caught compile failures, preserve the original exception, preserve fresh subsequent compilation, and retain the public immediate seed-then-compile component sequence.
+- `tests/View/ViewCompilerEngineTest.php`: cache true/false, deletion recovery, compiled-path cleanup, and both cache-reset entry points.
+- `tests/View/ViewComponentTest.php` and `ViewStaticStateTest.php`: internal methods stay out of component data and standardized cleanup resets every static field.
+- Raw Testbench and shared Mockery-lifecycle tests: central exception-handler ownership, exhaustive teardown ordering, and first-failure preservation without a second framework-reset registry.
+- `tests/View/ViewEngineResolverTest.php`, provider/config tests, and new `tests/View/PackageMetadataTest.php`: visibility, canonical defaults, lifecycle docs, and split metadata.
+- `tests/Integration/View`: nested-root, path-prefix sibling, trailing-separator duplicate, filesystem-root, and current upstream full-app View scenarios.
+- Mail View tests: cloned Factory/finder isolation remains intact.
+
+Tests must be deterministic. Concurrency tests use explicit channels/barriers, not sleeps. Publication tests use owned temporary directories, prove View delegates to the existing replacement boundary, and do not duplicate Filesystem's atomicity tests. Avoid Reflection and closure-bound protected-state inspection where a public/protected path can prove the behavior. The only approved closure-bound state assertions are the slot/slot-stack cleanup invariant and the bounded component-cache key, neither of which has observable public output.
+
+### Validation sequence
+
+1. Regenerate facades with `composer facade` after concrete API changes.
+2. During implementation, run PHP CS Fixer on touched files and focused View, Foundation View command/config, Testbench View, Mail View, and package metadata tests.
+3. Run `composer fix` once as the authoritative full formatter, both-PHPStan, parallel, Testbench, and dogfood checkpoint.
+4. Confirm the regenerated View facade contains `@method static string parentPlaceholder(string $section = '')` and no `getParentPlaceholder` entry.
+5. Review the full diff against current Laravel and every caller/callee; check API names, coroutine ownership, cleanup, stale symbols/comments/docs, and hot-path cost.
+6. Request independent code review and resolve every finding before completion.
+
+## Records and completion
+
+Before implementation, set all three core routing-index bullets to this View work and name the carried `view-01` and `reflection-02` entries. During implementation:
+
+- replace the “later full `view` audit” marker inside the `view-01` and `reflection-02` rows of the cross-package dependency index with complete revalidation;
+- tick the core package checklist's `view` entry only in the final bookkeeping commit;
+- add one complete View ledger entry covering `view-02` through `view-41`, rejected machinery, performance, validation, and the detail-plan link;
+- amend the earlier `view-01` and `reflection-02` entries with View revalidation;
+- add only genuine cross-package rows: Foundation-owned config/command and exception-handler changes, the Boost reserved-name documentation, Testbench and Testing cleanup ownership, and the separately routed `translation-10` twin;
+- record the inherited parent-placeholder subclass limitation without presenting speculative machinery as unresolved work;
+- leave no TODO, deferral, compatibility alias, dead middleware/property/helper, or superseded documentation.
+
+The View package is complete only after focused and full gates are green, fresh self-review finds no unresolved issue, independent code review signs off, and all records describe the final code rather than decision history.
diff --git a/src/boost/docs/blade.md b/src/boost/docs/blade.md
index 74b18b640..a1590f4f1 100644
--- a/src/boost/docs/blade.md
+++ b/src/boost/docs/blade.md
@@ -1255,8 +1255,10 @@ By default, some keywords are reserved for Blade's internal use in order to rend
- `data`
- `flushCache`
+- `flushState`
- `forgetComponentsResolver`
- `forgetFactory`
+- `ignoredParameterNames`
- `render`
- `resolve`
- `resolveComponentsUsing`
diff --git a/src/boost/docs/views.md b/src/boost/docs/views.md
index 345f3c717..9eeccb67c 100644
--- a/src/boost/docs/views.md
+++ b/src/boost/docs/views.md
@@ -351,7 +351,7 @@ View::creator('profile', ProfileCreator::class);
## Optimizing Views
-By default, Blade template views are compiled on demand. When a request is executed that renders a view, Hypervel will determine if a compiled version of the view exists. If the file exists, Hypervel will then determine if the uncompiled view has been modified more recently than the compiled view. If the compiled view either does not exist, or the uncompiled view has been modified, Hypervel will recompile the view.
+By default, Blade template views are compiled on demand. The first time a worker renders a view, Hypervel will determine if a compiled version of the view exists. If the file exists, Hypervel will then determine if the uncompiled view has been modified more recently than the compiled view. If the compiled view either does not exist, or the uncompiled view has been modified, Hypervel will recompile the view. The worker will reuse that compiled view for subsequent requests.
Compiling views during the request may have a small negative impact on performance, so Hypervel provides the `view:cache` Artisan command to precompile all of the views utilized by your application. For increased performance, you may wish to run this command as part of your deployment process:
@@ -366,3 +366,5 @@ You may use the `view:clear` command to clear the view cache:
```shell
php artisan view:clear
```
+
+When editing views while application workers are running, you should run this command so the workers compile the updated views the next time they are rendered.
diff --git a/src/foundation/config/view.php b/src/foundation/config/view.php
index ff65a0f1f..a232d0af6 100644
--- a/src/foundation/config/view.php
+++ b/src/foundation/config/view.php
@@ -33,4 +33,52 @@
'VIEW_COMPILED_PATH',
realpath(storage_path('framework/views')) ?: storage_path('framework/views')
),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Relative View Hashes
+ |--------------------------------------------------------------------------
+ |
+ | When this option is enabled, compiled view names are hashed relative to
+ | the application's base path. This keeps the names stable when the same
+ | application is deployed to a different absolute path.
+ |
+ */
+
+ 'relative_hash' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Compiled View Cache
+ |--------------------------------------------------------------------------
+ |
+ | This option determines whether compiled views may be reused. Disabling
+ | the cache causes Blade templates to be compiled before every render.
+ |
+ */
+
+ 'cache' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Compiled View Extension
+ |--------------------------------------------------------------------------
+ |
+ | This option controls the file extension used for compiled Blade views.
+ |
+ */
+
+ 'compiled_extension' => 'php',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Compiled View Timestamps
+ |--------------------------------------------------------------------------
+ |
+ | When enabled, Blade compares source and compiled modification times to
+ | determine whether a view should be compiled again.
+ |
+ */
+
+ 'check_cache_timestamps' => true,
];
diff --git a/src/foundation/src/Bootstrap/HandleExceptions.php b/src/foundation/src/Bootstrap/HandleExceptions.php
index 7ad68dc2c..3c14fd27b 100644
--- a/src/foundation/src/Bootstrap/HandleExceptions.php
+++ b/src/foundation/src/Bootstrap/HandleExceptions.php
@@ -253,6 +253,9 @@ protected function getExceptionHandler(): ExceptionHandler
*/
public static function flushState(?TestCase $testCase = null): void
{
+ // AfterEachTestSubscriber resets framework static state after each test.
+ // This reset remains caller-driven because restoring PHPUnit's error
+ // handler requires the active test case.
if (is_null(static::$app)) {
return;
}
diff --git a/src/foundation/src/Console/ViewCacheCommand.php b/src/foundation/src/Console/ViewCacheCommand.php
index d99ff6f3b..9992058be 100644
--- a/src/foundation/src/Console/ViewCacheCommand.php
+++ b/src/foundation/src/Console/ViewCacheCommand.php
@@ -43,7 +43,7 @@ public function handle()
*/
protected function compileViews(Collection $views): void
{
- $compiler = $this->hypervel['view']->getEngineResolver()->resolve('blade')->getCompiler();
+ $compiler = $this->hypervel->make('view')->getEngineResolver()->resolve('blade')->getCompiler();
$views->map(function (SplFileInfo $file) use ($compiler) {
$this->components->task(' ' . $file->getRelativePathname(), null, OutputInterface::VERBOSITY_VERY_VERBOSE);
@@ -61,7 +61,7 @@ protected function compileViews(Collection $views): void
*/
protected function bladeFilesIn(array $paths): Collection
{
- $extensions = (new Collection($this->hypervel['view']->getExtensions()))
+ $extensions = (new Collection($this->hypervel->make('view')->getExtensions()))
->filter(fn ($value) => $value === 'blade')
->keys()
->map(fn ($extension) => "*.{$extension}")
@@ -81,10 +81,27 @@ protected function bladeFilesIn(array $paths): Collection
*/
protected function paths(): Collection
{
- $finder = $this->hypervel['view']->getFinder();
+ $finder = $this->hypervel->make('view')->getFinder();
- return (new Collection($finder->getPaths()))->merge(
+ $paths = (new Collection($finder->getPaths()))->merge(
(new Collection($finder->getHints()))->flatten()
- );
+ )->map(function (string $path): string {
+ $path = realpath($path) ?: $path;
+
+ return dirname($path) === $path ? $path : rtrim($path, DIRECTORY_SEPARATOR);
+ })->unique();
+
+ return $paths->reject(function (string $path) use ($paths): bool {
+ // Trimming before appending preserves one boundary separator for filesystem roots.
+ $boundary = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+
+ return $paths->contains(
+ fn (string $existing): bool => $existing !== $path
+ && str_starts_with(
+ $boundary,
+ rtrim($existing, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
+ )
+ );
+ })->values();
}
}
diff --git a/src/support/src/Facades/Blade.php b/src/support/src/Facades/Blade.php
index 76a489728..3fbf9960d 100644
--- a/src/support/src/Facades/Blade.php
+++ b/src/support/src/Facades/Blade.php
@@ -45,7 +45,7 @@
* @method static string compileEndComponentClass()
* @method static mixed sanitizeComponentAttribute(mixed $value)
* @method static string compileEndOnce()
- * @method static void stringable(callable|string $class, callable|null $handler = null)
+ * @method static void stringable(\Closure|string $class, callable|null $handler = null)
* @method static string compileEchos(string $value)
* @method static mixed applyEchoHandler(mixed $value)
*
diff --git a/src/support/src/Facades/View.php b/src/support/src/Facades/View.php
index 96e203a36..16b87d138 100644
--- a/src/support/src/Facades/View.php
+++ b/src/support/src/Facades/View.php
@@ -67,7 +67,7 @@
* @method static string stopSection(bool $overwrite = false)
* @method static string appendSection()
* @method static string yieldContent(string $section, \Hypervel\Contracts\View\View|string $default = '')
- * @method static string getParentPlaceholder(string $section = '')
+ * @method static string parentPlaceholder(string $section = '')
* @method static bool hasSection(string $name)
* @method static bool sectionMissing(string $name)
* @method static mixed getSection(string $name, string|null $default = null)
diff --git a/src/testbench/src/Foundation/Application.php b/src/testbench/src/Foundation/Application.php
index 907700d4a..eaf4238cd 100644
--- a/src/testbench/src/Foundation/Application.php
+++ b/src/testbench/src/Foundation/Application.php
@@ -5,40 +5,13 @@
namespace Hypervel\Testbench\Foundation;
use Closure;
-use Hypervel\Console\Application as Artisan;
-use Hypervel\Console\Commands\ScheduleListCommand;
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
-use Hypervel\Database\Eloquent\Factories\Factory;
-use Hypervel\Database\Eloquent\Model;
-use Hypervel\Database\Migrations\Migrator;
-use Hypervel\Database\Schema\Builder as SchemaBuilder;
-use Hypervel\Foundation\Bootstrap\HandleExceptions;
use Hypervel\Foundation\Bootstrap\LoadEnvironmentVariables;
-use Hypervel\Foundation\Console\AboutCommand;
-use Hypervel\Foundation\Console\RouteListCommand;
-use Hypervel\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
-use Hypervel\Foundation\Http\Middleware\PreventRequestForgery;
-use Hypervel\Foundation\Http\Middleware\PreventRequestsDuringMaintenance;
-use Hypervel\Foundation\Http\Middleware\TrimStrings;
-use Hypervel\Http\Middleware\TrustHosts;
-use Hypervel\Http\Middleware\TrustProxies;
-use Hypervel\Http\Resources\Json\JsonResource;
-use Hypervel\Http\Resources\JsonApi\JsonApiResource;
-use Hypervel\Mail\Markdown;
-use Hypervel\Queue\Console\WorkCommand;
-use Hypervel\Queue\Queue;
-use Hypervel\Routing\Middleware\ThrottleRequests;
use Hypervel\Support\Arr;
-use Hypervel\Support\EncodedHtmlString;
-use Hypervel\Support\Sleep;
-use Hypervel\Support\Str;
-use Hypervel\Testbench\Bootstrap\RegisterProviders;
use Hypervel\Testbench\Concerns\CreatesApplication;
use Hypervel\Testbench\Contracts\Config as ConfigContract;
use Hypervel\Testbench\Foundation\Bootstrap\EnsuresDefaultConfiguration;
use Hypervel\Testbench\Foundation\Bootstrap\LoadEnvironmentVariablesFromArray;
-use Hypervel\Validation\Validator;
-use Hypervel\View\Component;
class Application
{
@@ -362,47 +335,4 @@ protected function resolveApplicationConfiguration(ApplicationContract $app): vo
$this->resolveApplicationConfigurationFromTrait($app);
(new EnsuresDefaultConfiguration)->bootstrap($app);
}
-
- /**
- * Flush static state touched by Testbench application bootstrap tests.
- *
- * IMPORTANT: This is NOT the global framework static-state cleanup list.
- * Do not add general framework static-state resets here. Add them to
- * src/testing/src/PHPUnit/AfterEachTestSubscriber.php instead.
- *
- * @param object $instance active test instance, used to thread the running TestCase through to HandleExceptions::flushState()
- */
- public static function flushState(object $instance): void
- {
- AboutCommand::flushState();
- Artisan::forgetBootstrappers();
- Component::flushCache();
- Component::forgetComponentsResolver();
- Component::forgetFactory();
- ConvertEmptyStringsToNull::flushState();
- EncodedHtmlString::flushState();
- Factory::flushState();
- HandleExceptions::flushState($instance instanceof \PHPUnit\Framework\TestCase ? $instance : null);
- Env::flushState();
- JsonResource::flushState();
- JsonApiResource::flushState();
- Markdown::flushState();
- Migrator::flushState();
- Model::flushState();
- PreventRequestForgery::flushState();
- PreventRequestsDuringMaintenance::flushState();
- Queue::flushState();
- RegisterProviders::flushState();
- RouteListCommand::flushState();
- ScheduleListCommand::flushState();
- SchemaBuilder::flushState();
- Sleep::flushState();
- Str::flushState();
- ThrottleRequests::flushState();
- TrimStrings::flushState();
- TrustProxies::flushState();
- TrustHosts::flushState();
- Validator::flushState();
- WorkCommand::flushState();
- }
}
diff --git a/src/testbench/src/PHPUnit/TestCase.php b/src/testbench/src/PHPUnit/TestCase.php
index 32c77e0e6..32dea205a 100644
--- a/src/testbench/src/PHPUnit/TestCase.php
+++ b/src/testbench/src/PHPUnit/TestCase.php
@@ -4,6 +4,7 @@
namespace Hypervel\Testbench\PHPUnit;
+use Hypervel\Foundation\Bootstrap\HandleExceptions;
use Hypervel\Testbench\Concerns\HandlesAssertions;
use Hypervel\Testing\Concerns\InteractsWithMockery;
use Override;
@@ -24,7 +25,31 @@ class TestCase extends \PHPUnit\Framework\TestCase
#[Override]
protected function tearDown(): void
{
- $this->tearDownTheTestEnvironmentUsingMockery();
+ $exception = null;
+
+ try {
+ $this->flushExceptionHandlerState();
+ } catch (Throwable $throwable) {
+ $exception = $throwable;
+ }
+
+ try {
+ $this->tearDownTheTestEnvironmentUsingMockery();
+ } catch (Throwable $throwable) {
+ $exception ??= $throwable;
+ }
+
+ if ($exception !== null) {
+ throw $exception;
+ }
+ }
+
+ /**
+ * Flush the global exception-handler state.
+ */
+ protected function flushExceptionHandlerState(): void
+ {
+ HandleExceptions::flushState($this);
}
/**
diff --git a/src/testing/src/PHPUnit/AfterEachTestSubscriber.php b/src/testing/src/PHPUnit/AfterEachTestSubscriber.php
index 3d59c8f09..6316863eb 100644
--- a/src/testing/src/PHPUnit/AfterEachTestSubscriber.php
+++ b/src/testing/src/PHPUnit/AfterEachTestSubscriber.php
@@ -307,12 +307,10 @@ protected function flushFrameworkState(): void
\Hypervel\Validation\RulePlanCache::flushState();
\Hypervel\Validation\ValidationRuleParser::flushState();
\Hypervel\Validation\Validator::flushState();
- \Hypervel\View\Component::flushCache();
- \Hypervel\View\Component::forgetComponentsResolver();
- \Hypervel\View\Component::forgetFactory();
+ \Hypervel\View\Component::flushState();
\Hypervel\View\ComponentAttributeBag::flushState();
\Hypervel\View\DynamicComponent::flushState();
- \Hypervel\View\Engines\CompilerEngine::forgetCompiledOrNotExpired();
+ \Hypervel\View\Engines\CompilerEngine::flushState();
\Hypervel\View\Factory::flushMacros();
\Hypervel\View\View::flushState();
\Hypervel\WebSocketServer\Collector\FdCollector::flushState();
diff --git a/src/view/README.md b/src/view/README.md
index d67ed0829..de4ab43a3 100644
--- a/src/view/README.md
+++ b/src/view/README.md
@@ -1,4 +1,12 @@
-View for Hypervel
-===
+# Hypervel View
[](https://deepwiki.com/hypervel/view)
+
+Documentation: https://hypervel.org/docs/views
+
+## Differences From Laravel
+
+- `Blade::component()` accepts the component alias before the class name: `Blade::component('package-alert', Alert::class)`.
+- A `View` passed as section content is rendered before the content is stored, so later changes to that `View` instance do not affect the section.
+
+Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/View
diff --git a/src/view/composer.json b/src/view/composer.json
index 9685179c7..a434182da 100644
--- a/src/view/composer.json
+++ b/src/view/composer.json
@@ -33,12 +33,15 @@
"hypervel/database": "^0.4",
"hypervel/events": "^0.4",
"hypervel/filesystem": "^0.4",
- "hypervel/foundation": "^0.4",
"hypervel/http": "^0.4",
"hypervel/macroable": "^0.4",
"hypervel/reflection": "^0.4",
"hypervel/support": "^0.4",
- "hypervel/validation": "^0.4"
+ "symfony/http-foundation": "^8.1",
+ "symfony/http-kernel": "^8.1"
+ },
+ "suggest": {
+ "hypervel/foundation": "Required for the @vite, @viteReactRefresh, and @fonts directives."
},
"autoload": {
"psr-4": {
diff --git a/src/view/src/Compilers/BladeCompiler.php b/src/view/src/Compilers/BladeCompiler.php
index 960b58e44..f1261f81d 100644
--- a/src/view/src/Compilers/BladeCompiler.php
+++ b/src/view/src/Compilers/BladeCompiler.php
@@ -17,6 +17,7 @@
use Hypervel\Support\Traits\ReflectsClosures;
use Hypervel\View\Component;
use InvalidArgumentException;
+use ParseError;
class BladeCompiler extends Compiler implements CompilerInterface
{
@@ -44,12 +45,12 @@ class BladeCompiler extends Compiler implements CompilerInterface
use Concerns\CompilesUseStatements;
use ReflectsClosures;
- /*
+ /**
* Temporarily store the raw blocks found in the template.
*/
protected const RAW_BLOCKS_CONTEXT_KEY = '__view.raw_blocks';
- /*
+ /**
* Footer lines to be added to the template.
*/
protected const FOOTER_CONTEXT_KEY = '__view.footer';
@@ -60,7 +61,7 @@ class BladeCompiler extends Compiler implements CompilerInterface
protected const PATH_CONTEXT_KEY = '__view.path';
/**
- * The "regular" / legacy echo string format.
+ * The temporary echo format override for the current coroutine.
*/
protected const ECHO_FORMAT_CONTEXT_KEY = '__view.echo_format';
@@ -119,9 +120,9 @@ class BladeCompiler extends Compiler implements CompilerInterface
protected array $escapedTags = ['{{{', '}}}'];
/**
- * Array of footer lines to be added to the template.
+ * The "regular" / legacy echo string format.
*/
- protected array $footer = [];
+ protected string $echoFormat = 'e(%s)';
/**
* The array of anonymous component paths to search for components in.
@@ -248,54 +249,68 @@ public function setPath(string $path): void
*/
public function compileString(string $value): string
{
- CoroutineContext::set(static::FOOTER_CONTEXT_KEY, []);
- $result = '';
+ // Raw blocks are pass-owned; component hashes may be publicly seeded for this pass.
+ CoroutineContext::setMany([
+ static::LAST_SECTION_CONTEXT_KEY => '',
+ static::FOOTER_CONTEXT_KEY => [],
+ static::RAW_BLOCKS_CONTEXT_KEY => [],
+ ]);
- foreach ($this->prepareStringsForCompilationUsing as $callback) {
- $value = $callback($value);
- }
+ try {
+ $result = '';
- $value = $this->storeUncompiledBlocks($value);
+ foreach ($this->prepareStringsForCompilationUsing as $callback) {
+ $value = $callback($value);
+ }
- // First we will compile the Blade component tags. This is a precompile style
- // step which compiles the component Blade tags into @component directives
- // that may be used by Blade. Then we should call any other precompilers.
- $value = $this->compileComponentTags(
- $this->compileComments($value)
- );
+ $value = $this->storeUncompiledBlocks($value);
- foreach ($this->precompilers as $precompiler) {
- $value = $precompiler($value);
- }
+ // First we will compile the Blade component tags. This is a precompile style
+ // step which compiles the component Blade tags into @component directives
+ // that may be used by Blade. Then we should call any other precompilers.
+ $value = $this->compileComponentTags(
+ $this->compileComments($value)
+ );
- // Here we will loop through all of the tokens returned by the Zend lexer and
- // parse each one into the corresponding valid PHP. We will then have this
- // template as the correctly rendered PHP that can be rendered natively.
- foreach (token_get_all($value) as $token) {
- $result .= is_array($token) ? $this->parseToken($token) : $token;
- }
+ foreach ($this->precompilers as $precompiler) {
+ $value = $precompiler($value);
+ }
- if (! empty(CoroutineContext::get(static::RAW_BLOCKS_CONTEXT_KEY))) {
- $result = $this->restoreRawContent($result);
- }
+ // Here we will loop through all of the tokens returned by the Zend lexer and
+ // parse each one into the corresponding valid PHP. We will then have this
+ // template as the correctly rendered PHP that can be rendered natively.
+ foreach (token_get_all($value) as $token) {
+ $result .= is_array($token) ? $this->parseToken($token) : $token;
+ }
- // If there are any footer lines that need to get added to a template we will
- // add them here at the end of the template. This gets used mainly for the
- // template inheritance via the extends keyword that should be appended.
- $footer = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
- if (count($footer) > 0) {
- $result = $this->addFooters($result, $footer);
- }
+ if (CoroutineContext::get(static::RAW_BLOCKS_CONTEXT_KEY, []) !== []) {
+ $result = $this->restoreRawContent($result);
+ }
- if (! empty($this->echoHandlers)) {
- $result = $this->addBladeCompilerVariable($result);
- }
+ // If there are any footer lines that need to get added to a template we will
+ // add them here at the end of the template. This gets used mainly for the
+ // template inheritance via the extends keyword that should be appended.
+ $footers = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
+ if (count($footers) > 0) {
+ $result = $this->addFooters($result);
+ }
- return str_replace(
- ['##BEGIN-COMPONENT-CLASS##', '##END-COMPONENT-CLASS##'],
- '',
- $result
- );
+ if (! empty($this->echoHandlers)) {
+ $result = $this->addBladeCompilerVariable($result);
+ }
+
+ return str_replace(
+ ['##BEGIN-COMPONENT-CLASS##', '##END-COMPONENT-CLASS##'],
+ '',
+ $result
+ );
+ } finally {
+ // Caught failures must not accumulate pass state in coroutine or process-global context.
+ CoroutineContext::setMany([
+ static::RAW_BLOCKS_CONTEXT_KEY => [],
+ static::COMPONENT_HASH_STACK_CONTEXT_KEY => [],
+ ]);
+ }
}
/**
@@ -421,6 +436,9 @@ protected function compileComponentTags(string $value): string
return $this->getComponentTagCompiler()->compile($value);
}
+ /**
+ * Get the component tag compiler.
+ */
protected function getComponentTagCompiler(): ComponentTagCompiler
{
if (isset($this->componentTagCompiler)) {
@@ -439,13 +457,13 @@ protected function getComponentTagCompiler(): ComponentTagCompiler
*/
protected function restoreRawContent(string $result): string
{
- $rawBlocks = CoroutineContext::get(static::RAW_BLOCKS_CONTEXT_KEY);
+ $rawBlocks = CoroutineContext::get(static::RAW_BLOCKS_CONTEXT_KEY, []);
$result = preg_replace_callback('/' . $this->getRawPlaceholder('(\d+)') . '/', function ($matches) use ($rawBlocks) {
return $rawBlocks[$matches[1]];
}, $result);
- $rawBlocks = CoroutineContext::set(static::RAW_BLOCKS_CONTEXT_KEY, []);
+ CoroutineContext::set(static::RAW_BLOCKS_CONTEXT_KEY, []);
return $result;
}
@@ -461,10 +479,22 @@ protected function getRawPlaceholder(int|string $replace): string
/**
* Add the stored footers onto the given content.
*/
- protected function addFooters(string $result, array $footer): string
+ protected function addFooters(string $result): string
{
+ $footers = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
+
return ltrim($result, "\n")
- . "\n" . implode("\n", array_reverse($footer));
+ . "\n" . implode("\n", array_reverse($footers));
+ }
+
+ /**
+ * Push a footer onto the stack.
+ */
+ protected function pushFooter(string $footer): void
+ {
+ $footers = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
+ $footers[] = $footer;
+ CoroutineContext::set(static::FOOTER_CONTEXT_KEY, $footers);
}
/**
@@ -474,7 +504,7 @@ protected function parseToken(array $token): string
{
[$id, $content] = $token;
- if ($id == T_INLINE_HTML) {
+ if ($id === T_INLINE_HTML) {
foreach ($this->compilers as $type) {
$content = $this->{"compile{$type}"}($content);
}
@@ -574,7 +604,12 @@ protected function replaceFirstStatement(string $search, string $replace, string
*/
protected function hasEvenNumberOfParentheses(string $expression): bool
{
- $tokens = token_get_all('
+ */
+ protected function splitTopLevel(string $expression, int $limit): array
+ {
+ $expression = $this->stripParentheses($expression);
+
+ if (! str_contains($expression, ',')) {
+ return [$expression];
+ }
+
+ try {
+ $tokens = token_get_all('map(function ($segment) {
- return Str::kebab($segment);
- })->implode(':')
+ ? (new Stringable($class))->after('\View\Components\\')->explode('\\')
+ ->map(fn ($segment) => Str::kebab($segment))
+ ->implode(':')
: Str::kebab(class_basename($class));
}
@@ -940,25 +1029,31 @@ public function precompiler(callable $precompiler): void
*/
public function usingEchoFormat(string $format, callable $callback): string
{
- $originalEchoFormat = $this->getEchoFormat();
+ $hadOverride = CoroutineContext::has(static::ECHO_FORMAT_CONTEXT_KEY);
+ $previous = CoroutineContext::get(static::ECHO_FORMAT_CONTEXT_KEY);
- $this->setEchoFormat($format);
+ CoroutineContext::set(static::ECHO_FORMAT_CONTEXT_KEY, $format);
try {
- $output = call_user_func($callback);
+ return call_user_func($callback);
} finally {
- $this->setEchoFormat($originalEchoFormat);
+ if ($hadOverride) {
+ CoroutineContext::set(static::ECHO_FORMAT_CONTEXT_KEY, $previous);
+ } else {
+ CoroutineContext::forget(static::ECHO_FORMAT_CONTEXT_KEY);
+ }
}
-
- return $output;
}
/**
* Set the echo format to be used by the compiler.
+ *
+ * Boot-only. The format persists on the singleton BladeCompiler for the
+ * worker lifetime and applies to every subsequent template compilation.
*/
public function setEchoFormat(string $format): void
{
- CoroutineContext::set(static::ECHO_FORMAT_CONTEXT_KEY, $format);
+ $this->echoFormat = $format;
}
/**
@@ -966,11 +1061,14 @@ public function setEchoFormat(string $format): void
*/
protected function getEchoFormat(): string
{
- return CoroutineContext::get(static::ECHO_FORMAT_CONTEXT_KEY, 'e(%s)');
+ return CoroutineContext::get(static::ECHO_FORMAT_CONTEXT_KEY, $this->echoFormat);
}
/**
* Set the "echo" format to double encode entities.
+ *
+ * Boot-only. The format persists on the singleton BladeCompiler for the
+ * worker lifetime and applies to every subsequent template compilation.
*/
public function withDoubleEncoding(): void
{
@@ -979,6 +1077,9 @@ public function withDoubleEncoding(): void
/**
* Set the "echo" format to not double encode entities.
+ *
+ * Boot-only. The format persists on the singleton BladeCompiler for the
+ * worker lifetime and applies to every subsequent template compilation.
*/
public function withoutDoubleEncoding(): void
{
@@ -987,16 +1088,12 @@ public function withoutDoubleEncoding(): void
/**
* Indicate that component tags should not be compiled.
+ *
+ * Boot-only. The setting persists on the singleton BladeCompiler for the
+ * worker lifetime and applies to every subsequent template compilation.
*/
public function withoutComponentTags(): void
{
$this->compilesComponentTags = false;
}
-
- protected function pushFooter($footer)
- {
- $stack = CoroutineContext::get(static::FOOTER_CONTEXT_KEY, []);
- $stack[] = $footer;
- CoroutineContext::set(static::FOOTER_CONTEXT_KEY, $stack);
- }
}
diff --git a/src/view/src/Compilers/Compiler.php b/src/view/src/Compilers/Compiler.php
index 96820214e..535052a54 100755
--- a/src/view/src/Compilers/Compiler.php
+++ b/src/view/src/Compilers/Compiler.php
@@ -32,7 +32,7 @@ public function __construct(
*/
public function getCompiledPath(string $path): string
{
- return $this->cachePath . '/' . hash('xxh128', 'v2' . Str::after($path, $this->basePath)) . '.' . $this->compiledExtension;
+ return $this->cachePath . '/' . hash('xxh128', 'v3' . Str::after($path, $this->basePath)) . '.' . $this->compiledExtension;
}
/**
diff --git a/src/view/src/Compilers/ComponentTagCompiler.php b/src/view/src/Compilers/ComponentTagCompiler.php
index 3f342b73a..036e59068 100644
--- a/src/view/src/Compilers/ComponentTagCompiler.php
+++ b/src/view/src/Compilers/ComponentTagCompiler.php
@@ -390,6 +390,10 @@ public function findClassByComponent(string $component): ?string
return $class;
}
+ if (class_exists($class = $class . '\\' . Str::afterLast($class, '\\'))) {
+ return $class;
+ }
+
return null;
}
@@ -405,6 +409,9 @@ public function guessClassName(string $component): string
return $namespace . 'View\Components\\' . $class;
}
+ /**
+ * Get the application namespace.
+ */
protected function getNamespace(): string
{
if (isset($this->namespace)) {
@@ -527,7 +534,9 @@ public function compileSlots(string $value): string
/x";
$value = preg_replace_callback($pattern, function ($matches) {
- $name = $this->stripQuotes($matches['inlineName'] ?: $matches['name'] ?: $matches['boundName']);
+ $name = $this->stripQuotes(
+ $matches['inlineName'] ?: $matches['name'] ?: $matches['boundName']
+ ) ?: "'slot'";
if (Str::contains($name, '-') && ! empty($matches['inlineName'])) {
$name = Str::camel($name);
diff --git a/src/view/src/Compilers/Concerns/CompilesConditionals.php b/src/view/src/Compilers/Concerns/CompilesConditionals.php
index d1a2884ec..363164cdb 100644
--- a/src/view/src/Compilers/Concerns/CompilesConditionals.php
+++ b/src/view/src/Compilers/Concerns/CompilesConditionals.php
@@ -301,9 +301,10 @@ protected function compileSelected(string $condition): string
*/
protected function compilePushIf(string $expression): string
{
- $parts = explode(',', $this->stripParentheses($expression), 2);
+ $parts = $this->splitTopLevel($expression, 2);
+ $stack = trim($parts[1]);
- return "startPush({$parts[1]}); ?>";
+ return "startPush({$stack}); ?>";
}
/**
@@ -311,9 +312,10 @@ protected function compilePushIf(string $expression): string
*/
protected function compileElsePushIf(string $expression): string
{
- $parts = explode(',', $this->stripParentheses($expression), 2);
+ $parts = $this->splitTopLevel($expression, 2);
+ $stack = trim($parts[1]);
- return "stopPush(); elseif({$parts[0]}): \$__env->startPush({$parts[1]}); ?>";
+ return "stopPush(); elseif({$parts[0]}): \$__env->startPush({$stack}); ?>";
}
/**
diff --git a/src/view/src/Compilers/Concerns/CompilesContexts.php b/src/view/src/Compilers/Concerns/CompilesContexts.php
index 7c2ff24fa..d32cd0590 100644
--- a/src/view/src/Compilers/Concerns/CompilesContexts.php
+++ b/src/view/src/Compilers/Concerns/CompilesContexts.php
@@ -22,7 +22,7 @@ protected function compileContext(string $expression): string
/**
* Compile the endcontext statements into valid PHP.
*/
- protected function compileEndcontext(): string
+ protected function compileEndcontext(?string $expression): string
{
return 'firstClosureParameterType($class), $class];
diff --git a/src/view/src/Compilers/Concerns/CompilesErrors.php b/src/view/src/Compilers/Concerns/CompilesErrors.php
index 5e5bd61e1..c794979ff 100644
--- a/src/view/src/Compilers/Concerns/CompilesErrors.php
+++ b/src/view/src/Compilers/Concerns/CompilesErrors.php
@@ -23,7 +23,7 @@ protected function compileError(string $expression): string
/**
* Compile the enderror statements into valid PHP.
*/
- protected function compileEnderror(): string
+ protected function compileEnderror(?string $expression): string
{
return 'stripParentheses($expression));
+ $parts = $this->splitTopLevel($expression, 3);
$options = isset($parts[1]) ? trim($parts[1]) : $this->encodingOptions;
diff --git a/src/view/src/Compilers/Concerns/CompilesLayouts.php b/src/view/src/Compilers/Concerns/CompilesLayouts.php
index 2704d0457..d1f5811ec 100644
--- a/src/view/src/Compilers/Concerns/CompilesLayouts.php
+++ b/src/view/src/Compilers/Concerns/CompilesLayouts.php
@@ -4,12 +4,14 @@
namespace Hypervel\View\Compilers\Concerns;
+use Hypervel\Context\CoroutineContext;
+
trait CompilesLayouts
{
/**
- * The name of the last section that was started.
+ * The name of the last section started during the current compile pass.
*/
- protected ?string $lastSection = null;
+ protected const LAST_SECTION_CONTEXT_KEY = '__view.last_section';
/**
* Compile the extends statements into valid PHP.
@@ -44,7 +46,7 @@ protected function compileExtendsFirst(string $expression): string
*/
protected function compileSection(string $expression): string
{
- $this->lastSection = trim($expression, "()'\" ");
+ CoroutineContext::set(static::LAST_SECTION_CONTEXT_KEY, trim($expression, "()'\" "));
return "startSection{$expression}; ?>";
}
@@ -54,9 +56,10 @@ protected function compileSection(string $expression): string
*/
protected function compileParent(): string
{
- $escapedLastSection = strtr($this->lastSection, ['\\' => '\\\\', "'" => "\\'"]);
+ $lastSection = CoroutineContext::get(static::LAST_SECTION_CONTEXT_KEY, '');
+ $escapedLastSection = strtr($lastSection, ['\\' => '\\\\', "'" => "\\'"]);
- return "getParentPlaceholder('{$escapedLastSection}'); ?>";
+ return "";
}
/**
diff --git a/src/view/src/Compilers/Concerns/CompilesSessions.php b/src/view/src/Compilers/Concerns/CompilesSessions.php
index 7f2327da9..ac128e96b 100644
--- a/src/view/src/Compilers/Concerns/CompilesSessions.php
+++ b/src/view/src/Compilers/Concerns/CompilesSessions.php
@@ -22,7 +22,7 @@ protected function compileSession(string $expression): string
/**
* Compile the endsession statements into valid PHP.
*/
- protected function compileEndsession(): string
+ protected function compileEndsession(?string $expression): string
{
return 'stripParentheses($expression), 2);
+ $parts = $this->splitTopLevel($expression, 2);
[$stack, $id] = [$parts[0], $parts[1] ?? ''];
@@ -68,7 +68,7 @@ protected function compilePrepend(string $expression): string
*/
protected function compilePrependOnce(string $expression): string
{
- $parts = explode(',', $this->stripParentheses($expression), 2);
+ $parts = $this->splitTopLevel($expression, 2);
[$stack, $id] = [$parts[0], $parts[1] ?? ''];
diff --git a/src/view/src/Component.php b/src/view/src/Component.php
index 71286fccd..8cc36c603 100644
--- a/src/view/src/Component.php
+++ b/src/view/src/Component.php
@@ -10,6 +10,7 @@
use Hypervel\Contracts\Support\Htmlable;
use Hypervel\Contracts\View\Factory;
use Hypervel\Contracts\View\View as ViewContract;
+use Hypervel\Filesystem\Filesystem;
use Hypervel\Support\Collection;
use ReflectionClass;
use ReflectionMethod;
@@ -152,7 +153,7 @@ public function resolveView(): ViewContract|Htmlable|Closure|string
*/
protected function extractBladeViewFromString(string $contents): string
{
- $key = sprintf('%s::%s', static::class, $contents);
+ $key = hash('xxh128', sprintf('%s::%s', static::class, $contents));
if (isset(static::$bladeViewCache[$key])) {
return static::$bladeViewCache[$key];
@@ -170,17 +171,16 @@ protected function extractBladeViewFromString(string $contents): string
*/
protected function createBladeViewFromString(Factory $factory, string $contents): string
{
- $factory->addNamespace(
- '__components',
- $directory = Container::getInstance()->make('config')->string('view.compiled')
- );
-
- if (! is_file($viewFile = $directory . '/' . hash('xxh128', $contents) . '.blade.php')) {
- if (! is_dir($directory)) {
- mkdir($directory, 0755, true);
- }
+ $container = Container::getInstance();
+ $files = $container->make(Filesystem::class);
+ $directory = $container->make('config')->string('view.compiled');
+ $viewFile = $directory . '/' . hash('xxh128', $contents) . '.blade.php';
+
+ $factory->replaceNamespace('__components', $directory);
- file_put_contents($viewFile, $contents);
+ if (! $files->exists($viewFile) || $files->size($viewFile) !== strlen($contents)) {
+ $files->ensureDirectoryExists($directory);
+ $files->replace($viewFile, $contents);
}
return '__components::' . basename($viewFile, '.blade.php');
@@ -207,8 +207,7 @@ protected function extractPublicProperties(): array
$reflection = new ReflectionClass($this);
static::$propertyCache[$class] = (new Collection($reflection->getProperties(ReflectionProperty::IS_PUBLIC)))
- ->reject(fn (ReflectionProperty $property) => $property->isStatic())
- ->reject(fn (ReflectionProperty $property) => $this->shouldIgnore($property->getName()))
+ ->reject(fn (ReflectionProperty $property) => $property->isStatic() || $this->shouldIgnore($property->getName()))
->map(fn (ReflectionProperty $property) => $property->getName())
->all();
}
@@ -290,8 +289,10 @@ protected function ignoredMethods(): array
'withName',
'withAttributes',
'flushCache',
+ 'flushState',
'forgetFactory',
'forgetComponentsResolver',
+ 'ignoredParameterNames',
'resolveComponentsUsing',
], $this->except);
}
@@ -388,6 +389,7 @@ public static function flushCache(): void
static::$constructorParametersCache = [];
static::$methodCache = [];
static::$propertyCache = [];
+ static::$ignoredParameterNames = [];
}
/**
@@ -428,4 +430,14 @@ public static function resolveComponentsUsing(Closure $resolver): void
{
static::$componentsResolver = $resolver;
}
+
+ /**
+ * Flush all static state.
+ */
+ public static function flushState(): void
+ {
+ static::flushCache();
+ static::forgetFactory();
+ static::forgetComponentsResolver();
+ }
}
diff --git a/src/view/src/ComponentAttributeBag.php b/src/view/src/ComponentAttributeBag.php
index 8663f4a97..e2f820c4e 100644
--- a/src/view/src/ComponentAttributeBag.php
+++ b/src/view/src/ComponentAttributeBag.php
@@ -68,6 +68,8 @@ public function get(string $key, mixed $default = null): mixed
/**
* Retrieve data from the instance.
+ *
+ * @return ($key is null ? array : mixed)
*/
protected function data(?string $key = null, mixed $default = null): mixed
{
@@ -222,6 +224,10 @@ public function merge(array $attributeDefaults = [], bool $escape = true): stati
if ($key === 'style') {
$value = Str::finish($value, ';');
+
+ if (is_string($defaultsValue) && $defaultsValue !== '') {
+ $defaultsValue = Str::finish($defaultsValue, ';');
+ }
}
return [$key => implode(' ', array_unique(array_filter([$defaultsValue, $value])))];
diff --git a/src/view/src/ComponentSlot.php b/src/view/src/ComponentSlot.php
index 76101006a..c81f38630 100644
--- a/src/view/src/ComponentSlot.php
+++ b/src/view/src/ComponentSlot.php
@@ -61,6 +61,8 @@ public function isNotEmpty(): bool
/**
* Determine if the slot has non-comment content.
+ *
+ * @throws InvalidArgumentException
*/
public function hasActualContent(callable|string|null $callable = null): bool
{
diff --git a/src/view/src/Concerns/ManagesComponents.php b/src/view/src/Concerns/ManagesComponents.php
index 0474f4339..12c130be9 100644
--- a/src/view/src/Concerns/ManagesComponents.php
+++ b/src/view/src/Concerns/ManagesComponents.php
@@ -52,6 +52,9 @@ public function startComponent(View|Htmlable|Closure|string $view, array $data =
}
}
+ /**
+ * Push a component onto the stack.
+ */
protected function pushComponentStack(View|Htmlable|Closure|string $view): int
{
$componentStack = CoroutineContext::get(static::COMPONENT_STACK_CONTEXT_KEY, []);
@@ -61,6 +64,9 @@ protected function pushComponentStack(View|Htmlable|Closure|string $view): int
return count($componentStack);
}
+ /**
+ * Pop the current component from the stack.
+ */
protected function popComponentStack(): View|Htmlable|Closure|string|null
{
$componentStack = CoroutineContext::get(static::COMPONENT_STACK_CONTEXT_KEY, []);
@@ -70,6 +76,9 @@ protected function popComponentStack(): View|Htmlable|Closure|string|null
return $view;
}
+ /**
+ * Append data for the current component.
+ */
protected function appendComponentData(array $data): void
{
$componentData = CoroutineContext::get(static::COMPONENT_DATA_CONTEXT_KEY, []);
@@ -77,7 +86,10 @@ protected function appendComponentData(array $data): void
CoroutineContext::set(static::COMPONENT_DATA_CONTEXT_KEY, $componentData);
}
- protected function createSlotContext()
+ /**
+ * Create the slot context for the current component.
+ */
+ protected function createSlotContext(): void
{
$slots = CoroutineContext::get(static::SLOTS_CONTEXT_KEY, []);
$slots[$this->currentComponent()] = [];
@@ -194,6 +206,9 @@ public function slot(string $name, ?string $content = null, array $attributes =
}
}
+ /**
+ * Set slot data for the current component.
+ */
protected function setSlotData(string $name, string|ComponentSlot|null $content): void
{
$currentComponent = $this->currentComponent();
@@ -203,6 +218,9 @@ protected function setSlotData(string $name, string|ComponentSlot|null $content)
CoroutineContext::set(static::SLOTS_CONTEXT_KEY, $slots);
}
+ /**
+ * Push a slot onto the stack.
+ */
protected function pushSlotStack(array $value): void
{
$currentComponent = $this->currentComponent();
@@ -212,6 +230,9 @@ protected function pushSlotStack(array $value): void
CoroutineContext::set(static::SLOT_STACK_CONTEXT_KEY, $slotStack);
}
+ /**
+ * Pop the current slot from the stack.
+ */
protected function popSlotStack(): array
{
$currentComponent = $this->currentComponent();
@@ -255,5 +276,7 @@ protected function flushComponents(): void
CoroutineContext::set(static::COMPONENT_STACK_CONTEXT_KEY, []);
CoroutineContext::set(static::COMPONENT_DATA_CONTEXT_KEY, []);
CoroutineContext::set(static::CURRENT_COMPONENT_DATA_CONTEXT_KEY, []);
+ CoroutineContext::set(static::SLOTS_CONTEXT_KEY, []);
+ CoroutineContext::set(static::SLOT_STACK_CONTEXT_KEY, []);
}
}
diff --git a/src/view/src/Concerns/ManagesLayouts.php b/src/view/src/Concerns/ManagesLayouts.php
index 3349f1521..5402ac1f2 100644
--- a/src/view/src/Concerns/ManagesLayouts.php
+++ b/src/view/src/Concerns/ManagesLayouts.php
@@ -22,9 +22,9 @@ trait ManagesLayouts
protected const SECTION_STACK_CONTEXT_KEY = '__view.section_stack';
/**
- * Context key for the parent placeholder.
+ * The parent placeholder salt for the worker.
*/
- protected const PARENT_PLACEHOLDER_CONTEXT_KEY = '__view.parent_placeholder';
+ protected static ?string $parentPlaceholderSalt = null;
/**
* Start injecting content into a section.
@@ -38,6 +38,7 @@ public function startSection(string $section, string|View|null $content = null):
CoroutineContext::set(static::SECTION_STACK_CONTEXT_KEY, $sectionStack);
}
} else {
+ // Coroutine contexts copy object references, so store immutable section content.
$this->extendSection($section, $content instanceof View ? $content->render() : e($content));
}
}
@@ -126,7 +127,7 @@ protected function extendSection(string $section, string $content): void
$sections = CoroutineContext::get(static::SECTIONS_CONTEXT_KEY, []);
if (isset($sections[$section])) {
- $content = str_replace($this->getParentPlaceholder($section), $content, $sections[$section]);
+ $content = str_replace(static::parentPlaceholder($section), $content, $sections[$section]);
}
$sections[$section] = $content;
@@ -138,36 +139,35 @@ protected function extendSection(string $section, string $content): void
*/
public function yieldContent(string $section, string|View $default = ''): string
{
- $sectionContent = $default instanceof View ? $default->render() : e($default);
-
$sections = CoroutineContext::get(static::SECTIONS_CONTEXT_KEY, []);
- if (isset($sections[$section])) {
- $sectionContent = $sections[$section];
- }
+ $sectionContent = isset($sections[$section])
+ ? $sections[$section]
+ : ($default instanceof View ? $default->render() : e($default));
$sectionContent = str_replace('@@parent', '--parent--holder--', $sectionContent);
return str_replace(
'--parent--holder--',
'@parent',
- str_replace($this->getParentPlaceholder($section), '', $sectionContent)
+ str_replace(static::parentPlaceholder($section), '', $sectionContent)
);
}
/**
- * Get the parent placeholder for the current request.
+ * Get the parent placeholder for the given section.
*/
- public function getParentPlaceholder(string $section = ''): string
+ public static function parentPlaceholder(string $section = ''): string
{
- $parentPlaceholder = CoroutineContext::get(static::PARENT_PLACEHOLDER_CONTEXT_KEY, []);
-
- if (! isset($parentPlaceholder[$section])) {
- $salt = Str::random(40);
- $parentPlaceholder[$section] = '##parent-placeholder-' . hash('xxh128', $salt . $section) . '##';
- CoroutineContext::set(static::PARENT_PLACEHOLDER_CONTEXT_KEY, $parentPlaceholder);
- }
+ // This pure value is cheaper to recompute than to retain in a worker-lived map.
+ return '##parent-placeholder-' . hash('xxh128', static::parentPlaceholderSalt() . $section) . '##';
+ }
- return $parentPlaceholder[$section];
+ /**
+ * Get the parent placeholder salt.
+ */
+ protected static function parentPlaceholderSalt(): string
+ {
+ return static::$parentPlaceholderSalt ??= Str::random(40);
}
/**
diff --git a/src/view/src/Concerns/ManagesLoops.php b/src/view/src/Concerns/ManagesLoops.php
index e7158f8ac..c449cbc17 100644
--- a/src/view/src/Concerns/ManagesLoops.php
+++ b/src/view/src/Concerns/ManagesLoops.php
@@ -33,7 +33,7 @@ public function addLoop(mixed $data): void
'remaining' => $length ?? null,
'count' => $length,
'first' => true,
- 'last' => isset($length) ? $length == 1 : null,
+ 'last' => isset($length) ? $length === 1 : null,
'odd' => false,
'even' => true,
'depth' => count($loopsStack) + 1,
@@ -54,11 +54,11 @@ public function incrementLoopIndices(): void
$loopsStack[$index] = array_merge($loopsStack[$index], [
'iteration' => $loop['iteration'] + 1,
'index' => $loop['iteration'],
- 'first' => $loop['iteration'] == 0,
+ 'first' => $loop['iteration'] === 0,
'odd' => ! $loop['odd'],
'even' => ! $loop['even'],
'remaining' => isset($loop['count']) ? $loop['remaining'] - 1 : null,
- 'last' => isset($loop['count']) ? $loop['iteration'] == $loop['count'] - 1 : null,
+ 'last' => isset($loop['count']) ? $loop['iteration'] === $loop['count'] - 1 : null,
]);
CoroutineContext::set(static::LOOPS_STACK_CONTEXT_KEY, $loopsStack);
@@ -93,4 +93,12 @@ public function getLoopStack(): array
{
return CoroutineContext::get(static::LOOPS_STACK_CONTEXT_KEY, []);
}
+
+ /**
+ * Flush the loop stack.
+ */
+ protected function flushLoops(): void
+ {
+ CoroutineContext::set(static::LOOPS_STACK_CONTEXT_KEY, []);
+ }
}
diff --git a/src/view/src/DynamicComponent.php b/src/view/src/DynamicComponent.php
index 15f07134c..14deba649 100644
--- a/src/view/src/DynamicComponent.php
+++ b/src/view/src/DynamicComponent.php
@@ -4,14 +4,22 @@
namespace Hypervel\View;
+use BackedEnum;
use Closure;
use Hypervel\Container\Container;
use Hypervel\Support\Collection;
use Hypervel\Support\Str;
use Hypervel\View\Compilers\ComponentTagCompiler;
+use function Hypervel\Support\enum_value;
+
class DynamicComponent extends Component
{
+ /**
+ * The name of the component.
+ */
+ public string $component;
+
/**
* The component tag compiler instance.
*/
@@ -25,9 +33,9 @@ class DynamicComponent extends Component
/**
* Create a new component instance.
*/
- public function __construct(
- public string $component
- ) {
+ public function __construct(BackedEnum|string $component)
+ {
+ $this->component = (string) enum_value($component);
}
/**
@@ -74,7 +82,7 @@ class_exists($class) ? '{{ $attributes }}' : '',
*/
protected function compileProps(array $bindings): string
{
- if (empty($bindings)) {
+ if ($bindings === []) {
return '';
}
@@ -99,8 +107,8 @@ protected function compileBindings(array $bindings): string
protected function compileSlots(array $slots): string
{
return (new Collection($slots))
- ->map(fn ($slot, $name) => $name === '__default' ? null : 'attributes) . '>{{ $' . $name . ' }}')
- ->filter()
+ ->reject(fn ($slot, $name) => $name === '__default')
+ ->map(fn ($slot, $name) => 'attributes) . '>{{ $' . $name . ' }}')
->implode(PHP_EOL);
}
@@ -109,11 +117,7 @@ protected function compileSlots(array $slots): string
*/
protected function classForComponent(): string
{
- if (isset(static::$componentClasses[$this->component])) {
- return static::$componentClasses[$this->component];
- }
-
- return static::$componentClasses[$this->component]
+ return static::$componentClasses[$this->component] ?? static::$componentClasses[$this->component]
= $this->compiler()->componentClass($this->component);
}
@@ -122,7 +126,7 @@ protected function classForComponent(): string
*/
protected function bindings(string $class): array
{
- [$data, $attributes] = $this->compiler()->partitionDataAndAttributes($class, $this->attributes->getAttributes());
+ [$data] = $this->compiler()->partitionDataAndAttributes($class, $this->attributes->getAttributes());
return array_keys($data->all());
}
diff --git a/src/view/src/Engines/CompilerEngine.php b/src/view/src/Engines/CompilerEngine.php
index 3c8f60283..ec8a64cc5 100755
--- a/src/view/src/Engines/CompilerEngine.php
+++ b/src/view/src/Engines/CompilerEngine.php
@@ -9,7 +9,7 @@
use Hypervel\Database\RecordsNotFoundException;
use Hypervel\Filesystem\Filesystem;
use Hypervel\Http\Exceptions\HttpResponseException;
-use Hypervel\Support\Str;
+use Hypervel\Support\Stringable;
use Hypervel\View\Compilers\CompilerInterface;
use Hypervel\View\ViewException;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -48,40 +48,44 @@ public function get(string $path, array $data = []): string
{
$this->pushCompiledPath($path);
- // If this given view has expired, which means it has simply been edited since
- // it was last compiled, we will re-compile the views so we can evaluate a
- // fresh copy of the view. We'll pass the compiler the path of the view.
- if (! isset(static::$compiledOrNotExpired[$path]) && $this->compiler->isExpired($path)) {
- $this->compiler->compile($path);
- }
-
- // Once we have the path to the compiled file, we will evaluate the paths with
- // typical PHP just like any other templates. We also keep a stack of views
- // which have been rendered for right exception messages to be generated.
-
try {
- $results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
- } catch (ViewException $e) {
- if (! Str::of($e->getMessage())->contains(['No such file or directory', 'File does not exist at path'])) {
- throw $e;
- }
-
+ // If this given view has expired, which means it has simply been edited since
+ // it was last compiled, we will re-compile the views so we can evaluate a
+ // fresh copy of the view. We'll pass the compiler the path of the view.
if (! isset(static::$compiledOrNotExpired[$path])) {
- throw $e;
+ if ($this->compiler->isExpired($path)) {
+ $this->compiler->compile($path);
+ } else {
+ static::$compiledOrNotExpired[$path] = true;
+ }
}
- $this->compiler->compile($path);
+ // Once we have the path to the compiled file, we will evaluate the paths with
+ // typical PHP just like any other templates. We also keep a stack of views
+ // which have been rendered for right exception messages to be generated.
+ try {
+ return $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
+ } catch (ViewException $e) {
+ if ((new Stringable($e->getMessage()))->doesntContain(['No such file or directory', 'File does not exist at path'])) {
+ throw $e;
+ }
- $results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
- }
-
- static::$compiledOrNotExpired[$path] = true;
+ if (! isset(static::$compiledOrNotExpired[$path])) {
+ throw $e;
+ }
- $this->popCompiledPath();
+ $this->compiler->compile($path);
- return $results;
+ return $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
+ }
+ } finally {
+ $this->popCompiledPath();
+ }
}
+ /**
+ * Push a compiled view path onto the stack.
+ */
protected function pushCompiledPath(string $path): void
{
$stack = CoroutineContext::get(static::COMPILED_PATH_CONTEXT_KEY, []);
@@ -89,6 +93,9 @@ protected function pushCompiledPath(string $path): void
CoroutineContext::set(static::COMPILED_PATH_CONTEXT_KEY, $stack);
}
+ /**
+ * Pop the current compiled view path from the stack.
+ */
protected function popCompiledPath(): void
{
$stack = CoroutineContext::get(static::COMPILED_PATH_CONTEXT_KEY, []);
@@ -121,7 +128,7 @@ protected function handleViewException(Throwable $e, int $obLevel): void
*/
protected function getMessage(Throwable $e): string
{
- $stack = CoroutineContext::get(static::COMPILED_PATH_CONTEXT_KEY);
+ $stack = CoroutineContext::get(static::COMPILED_PATH_CONTEXT_KEY, []);
return $e->getMessage() . ' (View: ' . realpath(last($stack)) . ')';
}
@@ -144,4 +151,12 @@ public static function forgetCompiledOrNotExpired(): void
{
static::$compiledOrNotExpired = [];
}
+
+ /**
+ * Flush all static state.
+ */
+ public static function flushState(): void
+ {
+ static::forgetCompiledOrNotExpired();
+ }
}
diff --git a/src/view/src/Engines/EngineResolver.php b/src/view/src/Engines/EngineResolver.php
index 4c8cc2090..835cc514b 100755
--- a/src/view/src/Engines/EngineResolver.php
+++ b/src/view/src/Engines/EngineResolver.php
@@ -24,6 +24,9 @@ class EngineResolver
* Register a new engine resolver.
*
* The engine string typically corresponds to a file extension.
+ *
+ * Boot-only. The resolver persists on the singleton EngineResolver for the
+ * worker lifetime and applies to every subsequent engine resolution.
*/
public function register(string $engine, Closure $resolver): void
{
@@ -52,6 +55,9 @@ public function resolve(string $engine): Engine
/**
* Remove a resolved engine.
+ *
+ * Boot or tests only. Clears a worker-wide resolved engine shared by every
+ * coroutine; the next resolution rebuilds it from its registered resolver.
*/
public function forget(string $engine): void
{
diff --git a/src/view/src/Factory.php b/src/view/src/Factory.php
index 1d76deae3..3e25a0d01 100755
--- a/src/view/src/Factory.php
+++ b/src/view/src/Factory.php
@@ -466,6 +466,7 @@ public function flushState(): void
$this->flushSections();
$this->flushStacks();
$this->flushComponents();
+ $this->flushLoops();
$this->flushFragments();
}
diff --git a/src/view/src/FileViewFinder.php b/src/view/src/FileViewFinder.php
index 28d7f2525..2aa205926 100755
--- a/src/view/src/FileViewFinder.php
+++ b/src/view/src/FileViewFinder.php
@@ -49,17 +49,17 @@ public function __construct(
/**
* Get the fully qualified location of the view.
*/
- public function find(string $name): string
+ public function find(string $view): string
{
- if (isset($this->views[$name])) {
- return $this->views[$name];
+ if (isset($this->views[$view])) {
+ return $this->views[$view];
}
- if ($this->hasHintInformation($name = trim($name))) {
- return $this->views[$name] = $this->findNamespacedView($name);
+ if ($this->hasHintInformation($view = trim($view))) {
+ return $this->views[$view] = $this->findNamespacedView($view);
}
- return $this->views[$name] = $this->findInPaths($name, $this->paths);
+ return $this->views[$view] = $this->findInPaths($view, $this->paths);
}
/**
diff --git a/src/view/src/Middleware/ValidationExceptionHandle.php b/src/view/src/Middleware/ValidationExceptionHandle.php
deleted file mode 100644
index 69951d506..000000000
--- a/src/view/src/Middleware/ValidationExceptionHandle.php
+++ /dev/null
@@ -1,71 +0,0 @@
-withErrors($request, $throwable->errors(), $throwable->errorBag);
-
- return redirect($request->session()->previousUrl());
- }
-
- throw $throwable;
- }
-
- return $response;
- }
-
- /**
- * Flash the validation errors to the session.
- */
- public function withErrors(Request $request, mixed $provider, string $key = 'default'): static
- {
- $value = $this->parseErrors($provider);
-
- $errors = $request->session()->get('errors', new ViewErrorBag);
-
- if (! $errors instanceof ViewErrorBag) {
- $errors = new ViewErrorBag;
- }
-
- $request->session()->flash(
- 'errors',
- $errors->put($key, $value)
- );
-
- return $this;
- }
-
- /**
- * Parse the given errors into a MessageBag instance.
- */
- protected function parseErrors(mixed $provider): MessageBagContract
- {
- if ($provider instanceof MessageProvider) {
- return $provider->getMessageBag();
- }
-
- return new MessageBag((array) $provider);
- }
-}
diff --git a/src/view/src/View.php b/src/view/src/View.php
index 82c5a04b2..ca1a5dee2 100755
--- a/src/view/src/View.php
+++ b/src/view/src/View.php
@@ -317,33 +317,33 @@ public function getEngine(): Engine
/**
* Determine if a piece of data is bound.
*/
- public function offsetExists(mixed $key): bool
+ public function offsetExists(mixed $offset): bool
{
- return array_key_exists($key, $this->data);
+ return array_key_exists($offset, $this->data);
}
/**
* Get a piece of bound data to the view.
*/
- public function offsetGet(mixed $key): mixed
+ public function offsetGet(mixed $offset): mixed
{
- return $this->data[$key];
+ return $this->data[$offset];
}
/**
* Set a piece of data on the view.
*/
- public function offsetSet(mixed $key, mixed $value): void
+ public function offsetSet(mixed $offset, mixed $value): void
{
- $this->with($key, $value);
+ $this->with($offset, $value);
}
/**
* Unset a piece of data from the view.
*/
- public function offsetUnset(mixed $key): void
+ public function offsetUnset(mixed $offset): void
{
- unset($this->data[$key]);
+ unset($this->data[$offset]);
}
/**
diff --git a/src/view/src/ViewServiceProvider.php b/src/view/src/ViewServiceProvider.php
index 9fbde8b2d..5fbf6e05b 100755
--- a/src/view/src/ViewServiceProvider.php
+++ b/src/view/src/ViewServiceProvider.php
@@ -29,7 +29,7 @@ public function register(): void
/**
* Register the view environment.
*/
- protected function registerFactory(): void
+ public function registerFactory(): void
{
$this->app->singleton('view', function ($app) {
// Next we need to grab the engine resolver instance that will be used by the
@@ -63,28 +63,28 @@ protected function createFactory(EngineResolver $resolver, ViewFinderInterface $
/**
* Register the view finder implementation.
*/
- protected function registerViewFinder(): void
+ public function registerViewFinder(): void
{
$this->app->bind('view.finder', function ($app) {
- return new FileViewFinder($app['files'], $app->make('config')->array('view.paths'));
+ return new FileViewFinder($app->make('files'), $app->make('config')->array('view.paths'));
});
}
/**
* Register the Blade compiler implementation.
*/
- protected function registerBladeCompiler(): void
+ public function registerBladeCompiler(): void
{
$this->app->singleton('blade.compiler', function ($app) {
$config = $app->make('config');
return tap(new BladeCompiler(
- $app['files'],
+ $app->make('files'),
$config->string('view.compiled'),
- $config->boolean('view.relative_hash', false) ? $app->basePath() : '',
- $config->boolean('view.cache', true),
- $config->string('view.compiled_extension', 'php'),
- $config->boolean('view.check_cache_timestamps', true),
+ $config->boolean('view.relative_hash') ? $app->basePath() : '',
+ $config->boolean('view.cache'),
+ $config->string('view.compiled_extension'),
+ $config->boolean('view.check_cache_timestamps'),
), function ($blade) {
$blade->component('dynamic-component', DynamicComponent::class);
});
@@ -94,7 +94,7 @@ protected function registerBladeCompiler(): void
/**
* Register the engine resolver instance.
*/
- protected function registerEngineResolver(): void
+ public function registerEngineResolver(): void
{
$this->app->singleton('view.engine.resolver', function () {
$resolver = new EngineResolver;
@@ -113,7 +113,7 @@ protected function registerEngineResolver(): void
/**
* Register the file engine implementation.
*/
- protected function registerFileEngine(EngineResolver $resolver): void
+ public function registerFileEngine(EngineResolver $resolver): void
{
$resolver->register('file', function () {
return new FileEngine(Container::getInstance()->make('files'));
@@ -123,7 +123,7 @@ protected function registerFileEngine(EngineResolver $resolver): void
/**
* Register the PHP engine implementation.
*/
- protected function registerPhpEngine(EngineResolver $resolver): void
+ public function registerPhpEngine(EngineResolver $resolver): void
{
$resolver->register('php', function () {
return new PhpEngine(Container::getInstance()->make('files'));
@@ -133,7 +133,7 @@ protected function registerPhpEngine(EngineResolver $resolver): void
/**
* Register the Blade engine implementation.
*/
- protected function registerBladeEngine(EngineResolver $resolver): void
+ public function registerBladeEngine(EngineResolver $resolver): void
{
$resolver->register('blade', function () {
$app = Container::getInstance();
diff --git a/tests/Foundation/Console/ViewCacheCommandTest.php b/tests/Foundation/Console/ViewCacheCommandTest.php
new file mode 100644
index 000000000..919db7da6
--- /dev/null
+++ b/tests/Foundation/Console/ViewCacheCommandTest.php
@@ -0,0 +1,94 @@
+files = new Filesystem;
+ $this->tempDir = ParallelTesting::tempDir('ViewCacheCommandTest');
+ $this->files->deleteDirectory($this->tempDir);
+ $this->files->ensureDirectoryExists($this->tempDir);
+ }
+
+ protected function tearDown(): void
+ {
+ $this->files->deleteDirectory($this->tempDir);
+
+ parent::tearDown();
+ }
+
+ public function testPathsCollapseNestedRootsWithoutConflatingPrefixSiblings(): void
+ {
+ $views = $this->tempDir . '/views';
+ $nested = $views . '/nested';
+ $viewsAdmin = $this->tempDir . '/views-admin';
+ $missing = $this->tempDir . '/missing';
+ $this->files->ensureDirectoryExists($nested);
+ $this->files->ensureDirectoryExists($viewsAdmin);
+
+ $command = $this->commandWithPaths([
+ $views,
+ $views . DIRECTORY_SEPARATOR,
+ $nested,
+ $viewsAdmin,
+ $missing . DIRECTORY_SEPARATOR,
+ $missing,
+ ]);
+
+ $this->assertSame([$views, $viewsAdmin, $missing], $command->viewPaths());
+ }
+
+ public function testPathsRetainFilesystemRoot(): void
+ {
+ $views = $this->tempDir . '/views';
+ $this->files->ensureDirectoryExists($views);
+ $command = $this->commandWithPaths([DIRECTORY_SEPARATOR, $views]);
+
+ $this->assertSame([DIRECTORY_SEPARATOR], $command->viewPaths());
+ }
+
+ protected function commandWithPaths(array $paths): TestViewCacheCommand
+ {
+ $finder = m::mock(ViewFinderInterface::class);
+ $finder->shouldReceive('getPaths')->once()->andReturn($paths);
+ $finder->shouldReceive('getHints')->once()->andReturn([]);
+
+ $factory = m::mock(Factory::class);
+ $factory->shouldReceive('getFinder')->once()->andReturn($finder);
+
+ $application = m::mock(Application::class);
+ $application->shouldReceive('make')->once()->with('view')->andReturn($factory);
+
+ $command = new TestViewCacheCommand;
+ $command->setHypervel($application);
+
+ return $command;
+ }
+}
+
+class TestViewCacheCommand extends ViewCacheCommand
+{
+ public function viewPaths(): array
+ {
+ return $this->paths()->all();
+ }
+}
diff --git a/tests/Foundation/FoundationConfigTest.php b/tests/Foundation/FoundationConfigTest.php
index 68bc32075..06fb56eef 100644
--- a/tests/Foundation/FoundationConfigTest.php
+++ b/tests/Foundation/FoundationConfigTest.php
@@ -108,6 +108,24 @@ public function testViewCompiledPathFallsBackToStoragePathWhenDirectoryDoesNotEx
}
}
+ public function testViewConfigDefinesCompilerDefaults(): void
+ {
+ $originalContainer = Container::getInstance();
+
+ try {
+ Container::setInstance(new Application(dirname(__DIR__, 2)));
+
+ $config = require dirname(__DIR__, 2) . '/src/foundation/config/view.php';
+ } finally {
+ Container::setInstance($originalContainer);
+ }
+
+ $this->assertFalse($config['relative_hash']);
+ $this->assertTrue($config['cache']);
+ $this->assertSame('php', $config['compiled_extension']);
+ $this->assertTrue($config['check_cache_timestamps']);
+ }
+
/**
* Load the application config with one environment override.
*/
diff --git a/tests/Integration/View/BladeAnonymousComponentTest.php b/tests/Integration/View/BladeAnonymousComponentTest.php
new file mode 100644
index 000000000..2cd81b1b9
--- /dev/null
+++ b/tests/Integration/View/BladeAnonymousComponentTest.php
@@ -0,0 +1,51 @@
+render();
+
+ $this->assertStringContainsString('Panel content.', $view);
+ $this->assertStringContainsString('class="app-layout"', $view);
+ $this->assertStringContainsString('class="danger-button"', $view);
+ }
+
+ public function testAnonymousComponentsWithCustomPathsCantBeRenderedAsNormalViews(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ Blade::anonymousComponentPath(__DIR__ . '/anonymous-components-1', 'layouts');
+ Blade::anonymousComponentPath(__DIR__ . '/anonymous-components-2');
+
+ View::make('layouts::app')->render();
+ }
+
+ public function testAnonymousComponentsWithCustomPathsCantBeRenderedAsNormalViewsEvenWithNoPrefix(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ Blade::anonymousComponentPath(__DIR__ . '/anonymous-components-1', 'layouts');
+ Blade::anonymousComponentPath(__DIR__ . '/anonymous-components-2');
+
+ View::make('panel')->render();
+ }
+
+ protected function defineEnvironment(ApplicationContract $app): void
+ {
+ $app['config']->set('view.paths', [__DIR__ . '/anonymous-components-templates']);
+ }
+}
diff --git a/tests/Integration/View/BladeTest.php b/tests/Integration/View/BladeTest.php
new file mode 100644
index 000000000..c0d0ef113
--- /dev/null
+++ b/tests/Integration/View/BladeTest.php
@@ -0,0 +1,270 @@
+artisan('view:clear');
+
+ parent::tearDown();
+ }
+
+ public function testRenderingBladeString(): void
+ {
+ $this->assertSame('Hello Taylor', Blade::render('Hello {{ $name }}', ['name' => 'Taylor']));
+ }
+
+ public function testRenderingBladeLongMaxpathlenString(): void
+ {
+ $longString = str_repeat('a', PHP_MAXPATHLEN);
+
+ $result = Blade::render($longString . '{{ $name }}', ['name' => 'a']);
+
+ $this->assertSame($longString . 'a', $result);
+ }
+
+ #[RunInSeparateProcess]
+ public function testRenderingBladeLongMaxpathlenStringWithExactLength(): void
+ {
+ for ($i = PHP_MAXPATHLEN - 200; $i <= PHP_MAXPATHLEN + 1; ++$i) {
+ $longString = str_repeat('x', $i);
+
+ $result = Blade::render($longString);
+
+ $this->assertSame($longString, $result);
+ }
+ }
+
+ public function testRenderingBladeComponentInstance(): void
+ {
+ $component = new HelloComponent('Taylor');
+
+ $this->assertSame('Hello Taylor', Blade::renderComponent($component));
+ }
+
+ public function testBasicBladeRendering(): void
+ {
+ $view = View::make('hello', ['name' => 'Taylor'])->render();
+
+ $this->assertSame('Hello Taylor', trim($view));
+ }
+
+ public function testRenderingAComponent(): void
+ {
+ $view = View::make('uses-panel', ['name' => 'Taylor'])->render();
+
+ $this->assertSame('