Skip to content

Let templates call presenter methods passed with the render - #176

Merged
schlessera merged 1 commit into
mainfrom
feat/173-presenters
Oct 24, 2026
Merged

schlessera merged 1 commit into
mainfrom
feat/173-presenters

Conversation

@schlessera

@schlessera schlessera commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Closes #173

What changed

A service can now pass a Presenter with View::render() and render_partial(), and the template calls its methods on $this. Only methods declared on an interface that extends Presenter are forwarded. String and stringable results are escaped like context properties, SafeHtml passes through as markup, and anything else comes back unchanged, so a boolean can drive an if. A partial inherits its parent's presenter unless given its own, and the presenter only holds for its own render. These calls fail the view through the existing debug/production policy: one with no presenter, one to an undeclared method, and any call on a presenter that declares a method the scope shadows (raw, render_partial, magic methods). The loop sample now reads its header through a PostHeader presenter (QueriedPostHeader, bound in the composition root and injected into SampleLoopService), and docs/patterns.md has a new "I need a template to ask for more than its context" section.

Why this shape

Ruled on #145 (revised decision, 2026-10-24). The earlier ruling put one plugin-wide ViewContext in the view factories. That object would grow with every subsystem, let every template call every method, make infrastructure views depend on the domain, and need #149's injector fix. Passing the presenter with the render keeps each template's contract to what its service gives it. No constructor, factory or injector changes.

Proof

composer test      # PHP 8.3: OK (626 tests); PHP 7.4 (dependencies resolved for 7.4): OK, 5 skipped
composer lint      # phpcs, PHPStan level max, Psalm, PHPStan rule tests: all clean on PHP 8.3
  • With /** @var TemplateScope&PostHeader $this */ in place, a misspelt $this->titel() in views/test-loop-service.php is reported by both PHPStan (method.notFound) and Psalm (UndefinedInterfaceMethod).
  • An independent review (gpt-6-astra) found four real issues, all fixed in this PR:
    • The presenter leaked past its render, so a nested render of the same view changed which presenter later partials inherited. It is now restored in finally.
    • The reserved-name test passed without the guard.
    • The result tests compared printed output rather than types.
    • A doc citation was stale.
  • Mutation checks: removing the presenter restore fails 2 tests, and removing the reserved-name guard fails 2 tests.

Checklist

  • composer test and composer lint pass locally, and CI is green on the current head.
  • Every acceptance criterion from the issue is met, or the gap is named below.
  • Nothing a generated plugin relies on changed, or the issue carries breaking and a ruling.
  • Docs and inline comments reflect the change, or nothing documented moved.

Anything left open

Breaking for custom View implementations, which must accept the new optional $presenter parameters. #175 records that the View interface still requires stdClass.

@schlessera schlessera added type: feat A new capability or a change in behaviour. area: infrastructure src/Infrastructure: services, injector, views, autoloader. What generated plugins build on. breaking Changes an interface or convention generated plugins rely on. Needs a ruling before code. labels Oct 24, 2026
A service can now pass a Presenter with View::render() and
render_partial(). The template scope forwards a method call on $this to the
presenter when an interface extending Presenter declares it. String and
stringable results are escaped like context properties, SafeHtml passes
through as markup, and other values come back unchanged. A call without a
presenter, an undeclared method or a presenter method shadowed by the scope
fails the view through the existing debug and production policy.

The loop sample now reads its header through a PostHeader presenter.

Closes #173
@schlessera
schlessera changed the base branch from fix/156-template-scope to main October 24, 2026 00:04
@schlessera
schlessera merged commit 1228940 into main Oct 24, 2026
5 checks passed
@schlessera
schlessera deleted the feat/173-presenters branch October 24, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: infrastructure src/Infrastructure: services, injector, views, autoloader. What generated plugins build on. breaking Changes an interface or convention generated plugins rely on. Needs a ruling before code. type: feat A new capability or a change in behaviour.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

views: templates call presenter methods passed with the render

1 participant