Repository navigation
feat: [#773] add View.Make to render a template to a string - #1561
darakanoit wants to merge 6 commits into
Conversation
Rendering now lives in the view module, so a view can be rendered outside a request, for example to warm a page cache from a command or a queued job. The API follows Laravel's split between the view factory and a view instance: View.Make and View.First return a Template, and Template.Render returns the HTML. Template also has With, Name and Data. Templates are resolved the same way the gin and fiber drivers do it: resources/views, then LoadViewsFrom directories, then LoadViewsFromFS filesystems, first definition wins. The parsed set is cached and reset when a new view source is registered.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1561 +/- ##
==========================================
+ Coverage 72.84% 72.99% +0.14%
==========================================
Files 412 414 +2
Lines 26846 27013 +167
==========================================
+ Hits 19557 19718 +161
- Misses 7287 7293 +6
Partials 2 2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ery render Pointer fields in struct data are dereferenced the way the gin and fiber drivers do it, so a nil pointer renders as an empty value instead of <nil>. The compiled template set is read through an atomic pointer, so concurrent renders no longer queue on a mutex. A parse error is kept with it, like the drivers keep theirs, instead of re-parsing every view source on each render. The output buffer is sized from the previous render of the same template, which avoids growing it and copying it at the end. Adds BenchmarkTemplateRender.
|
Pushed two more commits:
One difference from the drivers is left as is: if two files in |
🤖 Automated reviewThis is an AI-generated code review. Please double-check each finding before acting. SummaryPR #1561 adds Verdict
FindingsMust Fix
Should Fix
Nits
Automated Checks
|
Rendering looked views up in the template set html/template happens to end
up with, which holds more than the views themselves. A file that only holds
define blocks leaves an empty template named after it, and every set carries
an unnamed root, so Make("app.tmpl") on a resources/views that only has
layouts/app.tmpl returned an empty string and no error at all.
Names are now collected while the sources are parsed, and only those can be
rendered. A file contributes each of its define blocks and, when it has text
of its own around them, its path within the source as well as its base name,
so a nested file without a define block is addressable the way Exists and the
route drivers address it. First asks whether a candidate can be rendered
rather than whether the file exists, so it no longer settles on one it cannot
render, and it keeps the error from invalid data instead of replacing it.
Files are parsed one at a time and their templates moved into the set
individually, so a package that takes a name it does not own loses that name
and not the rest of the file. This drops the regular expression the sources
were scanned with, which missed {{- define }} and let a package override
every define in a file but the first.
Copying the data of a map[string]any no longer goes through reflect, which
boxed every value: 12 allocations become 2 for a small map. Data returns a
copy, like the accessors next to it.
|
Pushed a commit for the automated review above. Both Must Fix findings reproduce, and so does most of the rest. Rendering resolved names against the template set
Files are parsed one at a time and their templates are moved into the set individually. I did not take the suggested fix for the define precedence finding: skipping a package file whole because one of its names is taken costs the package every other template in that file, where installing name by name costs it just that name. This also drops the regular expression the sources were scanned with, which is what missed
Two behaviour notes for review. Three findings I did not act on. |
|
Some numbers behind the Limitations section, because the risk in it is easy to read past. The limitation. Delimiters and template functions are configured per driver, through The risks, worst first.
The error also names a file the caller never asked for, which makes it hard to act on.
What resolving this requires. The configuration cannot simply be read from where it is.
What can be done without deciding either. Parsing sources independently, so one failing view does not take the rest with it, would contain risks 2 and 3, and would also stop a stray non-template file in None of this affects the API this PR adds. |
🤖 Automated reviewThis is an AI-generated code review. Please double-check each finding before acting. You don't need to address every issue if they are inaccurate, but please point them out if any exist. SummaryPR #1561 adds Verdict
FindingsShould Fix
Nits
|
An empty define or a bare {{ block }} placeholder no longer counts as a
view, so Make reports it as missing and First falls through to the next
candidate. The name becomes renderable once a body for it is parsed,
whichever of the two is walked first.
When the sources fail to parse, First picks the first candidate so Render
reports the parse error, including for a candidate given by its base name.
A First that matched nothing names its candidates in a data error.
|
Pushed a commit for round 2. All four Should Fix findings reproduced. An empty When the sources fail to parse, From the nits: the Not changed:
|
|
Merged The |
🤖 Automated reviewThis is an AI-generated code review. Please double-check each finding before acting. You don't need to address every issue if they are inaccurate, but please point them out if any exist. SummaryPR #1561 adds Verdict
FindingsMust Fix
Should Fix
Nits
|
📑 Description
Closes goravel/goravel#773
Implements the first option from the discussion in the issue: rendering lives in the
viewmodule, so a view can be turned into a string outside a request, for exampleto warm a page cache from a command or a queued job.
The API follows Laravel's split between the view factory and a view instance.
View::make()in Laravel returns a view object andrender()on it produces thestring; here
facades.View().Make()returns aTemplateandRender()produces theHTML:
View::make($view, $data)facades.View().Make(view, data...)View::first($views, $data)facades.View().First(views, data...)$view->with($key, $value)template.With(key, value)$view->render()template.Render()$view->getName()template.Name()$view->getData()template.Data()View::exists()andView::share()already had counterparts (Exists,Share).There are two differences from Laravel.
Render()returns(string, error)instead ofthrowing.
MakeandFirstdon't return an error, so invalid data, or aFirstcallwhere none of the views exist, is reported when
Render()is called. Laravel throws infirst()right away.Templates are resolved the same way the gin and fiber drivers do it:
resources/viewsfirst, then directories from
LoadViewsFrom, then filesystems fromLoadViewsFromFS,and the first source that defines a template name wins.
Firstchecks candidates withExists, likectx.Response().View().First()does in the drivers. The data can be amap, a struct or a pointer to a struct. It is copied, so the caller's map is not
modified, and like in Laravel it is merged over the shared data at render time.
The template set is parsed on the first
Render()call and kept. Registering a newview source resets it, so views loaded in a provider's
Bootare picked up.Limitations
Two things differ from
ctx.Response().View().Make(), both mentioned in the issue:(
http.drivers.<driver>.template), soRender()uses thehtml/templatedefaults.Moving that config into
frameworkand letting the drivers render throughTemplate,the way Laravel's HTTP response renders a view object, can be a follow-up;
csrf_tokencomes from the request session and is not available here.Verification
Checked in an app built from the skeleton with gin, with
frameworkreplaced by thisbranch. An artisan command renders
welcome.tmpl, a view fromLoadViewsFromFSwithshared data and
With, and theFirst, invalid data and missing view cases. Over HTTP,welcome.tmplrendered withView().Make(...).Render()is byte for byte identical tothe gin driver's
ctx.Response().View().Make(...)output.Scope
contracts/view/view.go:MakeandFirstonView, newTemplatecontractview/template.go: theTemplateimplementationview/render.go: template loading and cachingview/view.go:Make,First, and resetting the cached set when a source is registerederrors/list.go:ViewInvalidData,ViewNoneExistmocks/view/: regenerated✅ Checks