Skip to content

Add parent() to render a shadowed template - #95

Merged
harikt merged 6 commits into
6.xfrom
render-stack
Jul 23, 2026
Merged

Add parent() to render a shadowed template#95
harikt merged 6 commits into
6.xfrom
render-stack

Conversation

@harikt

@harikt harikt commented Jul 23, 2026

Copy link
Copy Markdown
Member

Search paths are first-hit-wins, so a template in an earlier directory shadows one of the same name later in the list and the shadowed version is unreachable: find() returns the first hit and drops the rest of the chain. Changing one part of a package's template therefore meant copying the whole file, which then stops tracking the original.

TemplateRegistry now records which directory satisfied each name, and exposes two primitives on SearchPathInterface: getResolvedPath(), which answers "which package's template won?", and getNext(), which resumes the search after a given directory.

getNext() returns a ResolvedTemplate rather than a bare Closure. Walking a chain past the first step needs the path the parent itself was found in, and a closure does not carry that.

AbstractView gains a render stack -- render() pushes (name, path) and pops in a finally -- so parent() resolves against the template actually executing when renders nest, and a throwing template leaves no frame behind. The output buffering render() did inline moves to captureTemplate() so parent() shares it, and so the blocks port can reuse the same stack rather than introducing a second one.

parent() returns '' rather than throwing when there is nothing further to render: overriding a template that turns out to shadow nothing is a normal state during development, not an error.

Summary by CodeRabbit

  • New Features
    • Enhanced template inheritance so parent() can continue rendering shadowed templates across search paths, including nested inheritance.
    • Added “Strict Parent Mode” (setStrictParent()/isStrictParent()) with a new ParentNotFound exception when no further parent exists.
    • Added SearchPathInterface APIs to inspect resolution: getResolvedPath() and getNext().
  • Bug Fixes
    • Improved inheritance reliability via consistent path normalization and safer nested rendering state handling.
  • Documentation
    • Expanded guidance for extending shadowed templates and strict-parent behavior.
  • Tests
    • Added comprehensive coverage for chain walking, strict-parent cases, and trailing-separator normalization.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@harikt, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a51346a-13ae-434b-a96f-932539c5ded2

📥 Commits

Reviewing files that changed from the base of the PR and between 6aee208 and 6c2d4ce.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • docs/templates.md

Walkthrough

This change adds shadowed-template inheritance through parent(), exposes search-path traversal metadata, tracks nested renders, adds strict-parent exceptions, and documents and tests chained, namespaced, variable-aware, and failure-handling behavior.

Changes

Template inheritance and resolution

Layer / File(s) Summary
Resolution contracts and shadowed lookup
src/ResolvedTemplate.php, src/SearchPathInterface.php, src/TemplateRegistry.php, docs/interfaces.md, tests/TemplateRegistryTest.php
Search-path resolution reports the winning directory and returns subsequent shadowed templates through ResolvedTemplate; normalized paths and cache invalidation keep traversal metadata consistent.
Render-stack parent rendering
src/AbstractView.php, src/View.php, src/Exception/ParentNotFound.php, tests/ParentTest.php, tests/FakeRegistryWithoutPaths.php, tests/fixtures/parent/*
Views track active template frames, safely capture nested output, implement parent() across shadowing chains, and support strict failures and registries without search paths.
Inheritance documentation
docs/templates.md, CHANGELOG.md
Documentation describes shadowed-template composition, strict-parent behavior, resolution APIs, render-stack cleanup, and path normalization.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant View
  participant AbstractView
  participant TemplateRegistry
  View->>AbstractView: render template
  AbstractView->>TemplateRegistry: getResolvedPath(name)
  AbstractView->>TemplateRegistry: getNext(name, resolvedPath)
  TemplateRegistry-->>AbstractView: next ResolvedTemplate
  AbstractView->>AbstractView: captureTemplate(template, vars)
  AbstractView-->>View: rendered parent output
Loading

Poem

A bunny hops through paths in flight,
Parent templates bloom just right.
Stack frames rise, then safely fall,
Shadowed pages answer all.
“Hop!” says the rabbit—clean output!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding parent() support for rendering shadowed templates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch render-stack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/AbstractView.php`:
- Around line 396-408: Update captureTemplate to record the initial
output-buffer and capture/section-frame depths before invoking the template,
then restore both stacks in the Throwable catch path. Ensure all buffers and the
stale $capture entry created after those initial depths are removed before
rethrowing, while preserving normal successful capture behavior.

In `@src/TemplateRegistry.php`:
- Around line 248-259: Normalize configured paths by trimming trailing directory
separators in the path assignment logic used by setPaths() and setNamespaces(),
ensuring the canonical values stored in $paths and namespace path lists match
getNext()’s afterPath lookup. Preserve root-path handling, and add a regression
test covering parent() when a configured path has a trailing separator.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a32088b-0a26-4ebe-959b-65b9c4cbd389

📥 Commits

Reviewing files that changed from the base of the PR and between 767da4a and a796566.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • docs/interfaces.md
  • docs/templates.md
  • src/AbstractView.php
  • src/ResolvedTemplate.php
  • src/SearchPathInterface.php
  • src/TemplateRegistry.php
  • src/View.php
  • tests/FakeRegistryWithoutPaths.php
  • tests/ParentTest.php
  • tests/TemplateRegistryTest.php
  • tests/fixtures/parent/app/boom.php
  • tests/fixtures/parent/app/orphan.php
  • tests/fixtures/parent/app/outer.php
  • tests/fixtures/parent/app/read.php
  • tests/fixtures/parent/app/vars.php
  • tests/fixtures/parent/core/read.php
  • tests/fixtures/parent/core/vars.php
  • tests/fixtures/parent/module/read.php
  • tests/fixtures/parent/ns-app/read.php
  • tests/fixtures/parent/ns-core/read.php

Comment thread src/AbstractView.php
Comment thread src/TemplateRegistry.php
harikt added 5 commits July 23, 2026 22:53
Search paths are first-hit-wins, so a template in an earlier directory shadows
one of the same name later in the list and the shadowed version is unreachable:
find() returns the first hit and drops the rest of the chain. Changing one part
of a package's template therefore meant copying the whole file, which then stops
tracking the original.

TemplateRegistry now records which directory satisfied each name, and exposes
two primitives on SearchPathInterface: getResolvedPath(), which answers "which
package's template won?", and getNext(), which resumes the search after a given
directory.

getNext() returns a ResolvedTemplate rather than a bare Closure. Walking a chain
past the first step needs the path the parent itself was found in, and a closure
does not carry that.

AbstractView gains a render stack -- render() pushes (name, path) and pops in a
finally -- so parent() resolves against the template actually executing when
renders nest, and a throwing template leaves no frame behind. The output
buffering render() did inline moves to captureTemplate() so parent() shares it,
and so the blocks port can reuse the same stack rather than introducing a
second one.

parent() returns '' rather than throwing when there is nothing further to
render: overriding a template that turns out to shadow nothing is a normal
state during development, not an error.
prependPath() and appendPath() have always trimmed trailing directory
separators; setPaths() and setNamespaces() stored whatever they were given, so
one directory had two spellings inside the registry depending on which setter
registered it.

That is not merely untidy. The path recorded for a found template is handed
straight back to getNext() to resume the search, and getNext() trims before
comparing, so a directory registered with a trailing slash never matched its
own entry in the path list. parent() then returned '' -- reporting "this
template shadows nothing" for a template that plainly does, and going quiet
instead of failing loudly.

Normalizing at the setters keeps one spelling internally, which is what both
the search and the resumption assume.
The docs listed the exception alongside the three empty-string cases, which
reads as though a template author might hit it. They cannot: parent() is
protected, so reaching it means a template is running, and a running template
was rendered, and a render always has a frame.

The distinction is that '' answers a well-formed question -- there is a current
template and nothing follows it -- while the exception says there is no current
template to resume from, which only happens off the normal render path.

The case that justifies keeping it is a subclass that reimplements render()
without maintaining the render stack. Without the throw, every parent() in the
application would quietly return '' and every override would go back to
replacing instead of extending: wrong output, nothing raised. Add the test for
that case, since the docs now name it.
parent() returns '' when it finds nothing to resume into, which is right for
production: an override written before the template it overrides exists should
not take a page down.

But the same '' is what a misconfiguration produces. A typo in a search path,
paths registered in the wrong order, or a registry with no paths at all are
indistinguishable from "this template legitimately shadows nothing" -- overrides
quietly stop composing, the page still renders, and the only symptom is missing
markup. Both bugs found in this branch had exactly that shape.

setStrictParent(true) turns the three cases into Exception\ParentNotFound,
naming the template and why the lookup came up empty. Off by default.

It takes a bool rather than reading the environment. Aura.View has no config
layer and no runtime dependencies, so deciding what "development" means belongs
to whatever wires the View up.
The guard is still worth documenting -- strict parent mode covers a different
exception, so nothing here became redundant -- but two thirds of the passage
was a code block spelling out pushRender()/popRender()/captureTemplate().

That is protected API aimed at subclass authors, in a page written for template
authors, and it has to be kept in step with View::render() or it goes stale and
misleads. Point at the real method instead, and keep the one thing a subclass
author cannot guess: overriding render() without the stack silently disables
parent() application-wide.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/AbstractView.php (1)

353-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated bind-and-fallback logic between getTemplate() and parent().

getTemplate() (Line 353-357) and parent() (Line 499) both do $tmpl->bindTo($this, static::class) ?? $tmpl. Consider extracting a small private helper (e.g. bindTemplate(\Closure $tmpl): \Closure) to keep the binding rule in one place.

♻️ Proposed refactor
+    protected function bindTemplate(\Closure $tmpl): \Closure
+    {
+        return $tmpl->bindTo($this, static::class) ?? $tmpl;
+    }
+
     protected function getTemplate(string $name): \Closure
     {
         $tmpl = $this->template_registry->get($name);
-        return $tmpl->bindTo($this, static::class) ?? $tmpl;
+        return $this->bindTemplate($tmpl);
     }
-        $template = $next->template->bindTo($this, static::class) ?? $next->template;
+        $template = $this->bindTemplate($next->template);

Also applies to: 465-507

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/AbstractView.php` around lines 353 - 357, Extract the duplicated
closure-binding expression from getTemplate() and parent() into a private helper
such as bindTemplate(\Closure $tmpl): \Closure. Update both methods to call the
helper while preserving the existing bindTo($this, static::class) behavior and
fallback to the original closure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 241-243: Update the changelog entry for $capture and $section to
retain the explicit [] initialization while correcting the rationale: do not
claim that appending to null is deprecated in PHP 8.3; reference the PHP 8.1
deprecation of autovivification from false instead.

In `@docs/templates.md`:
- Around line 165-168: Add the `text` language tag to the fenced diagnostic
block containing the plain-text exception message in the documentation, without
changing the message content.

---

Nitpick comments:
In `@src/AbstractView.php`:
- Around line 353-357: Extract the duplicated closure-binding expression from
getTemplate() and parent() into a private helper such as bindTemplate(\Closure
$tmpl): \Closure. Update both methods to call the helper while preserving the
existing bindTo($this, static::class) behavior and fallback to the original
closure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 679ebad7-73fd-4c71-9b1e-a8b1a2af128b

📥 Commits

Reviewing files that changed from the base of the PR and between a796566 and 6aee208.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • docs/interfaces.md
  • docs/templates.md
  • src/AbstractView.php
  • src/Exception/ParentNotFound.php
  • src/ResolvedTemplate.php
  • src/SearchPathInterface.php
  • src/TemplateRegistry.php
  • src/View.php
  • tests/FakeRegistryWithoutPaths.php
  • tests/ParentTest.php
  • tests/TemplateRegistryTest.php
  • tests/fixtures/parent/app/boom.php
  • tests/fixtures/parent/app/orphan.php
  • tests/fixtures/parent/app/outer.php
  • tests/fixtures/parent/app/read.php
  • tests/fixtures/parent/app/vars.php
  • tests/fixtures/parent/core/read.php
  • tests/fixtures/parent/core/vars.php
  • tests/fixtures/parent/module/read.php
  • tests/fixtures/parent/ns-app/read.php
  • tests/fixtures/parent/ns-core/read.php
🚧 Files skipped from review as they are similar to previous changes (10)
  • tests/fixtures/parent/core/read.php
  • src/ResolvedTemplate.php
  • tests/fixtures/parent/app/boom.php
  • tests/fixtures/parent/app/vars.php
  • tests/fixtures/parent/app/outer.php
  • tests/fixtures/parent/ns-app/read.php
  • tests/fixtures/parent/core/vars.php
  • src/SearchPathInterface.php
  • docs/interfaces.md
  • src/TemplateRegistry.php

Comment thread CHANGELOG.md Outdated
Comment thread docs/templates.md Outdated
The CHANGELOG claimed appending to null is deprecated as of PHP 8.3. It is not:
null autovivifies to array silently, and it was autovivification from false that
PHP 8.1 deprecated -- verified on 8.4. Reword to say the explicit [] just avoids
depending on either.

Tag the strict-parent diagnostic block as text so it stops tripping markdown
lint.
@harikt
harikt merged commit 0b3fc24 into 6.x Jul 23, 2026
8 of 10 checks passed
@harikt
harikt deleted the render-stack branch July 23, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant