bernhardh/nova-dynamic-views
A Laravel Nova package for creating dynamic views of your resources, letting you present tailored resource tables without duplicating Nova resource classes for every variation. Useful for adapting indexes to different workflows and teams.
Treat this as a small, single-purpose Nova add-on, not a framework. Read the package source before the README — with 16 stars and an archived status, docs are thin and the src/ServiceProvider.php plus any façade/manager class are the real specification.
Install:
composer require bernhardh/nova-dynamic-views
Then inspect what the provider registers:
cat vendor/bernhardh/nova-dynamic-views/src/*ServiceProvider.php
If the provider publishes config or a placeholder registry, publish it and check the tag name in the provider's publishes() call:
php artisan vendor:publish --provider="Bernhardh\\NovaDynamicViews\\NovaDynamicViewsServiceProvider"
Where to look first, in order:
Nova::.composer.json — the illuminate/* and laravel/nova constraints tell you which Nova major you can run it on. This package last shipped 2022-11-02, so assume Nova 3/4-era targeting until proven otherwise.First use case — the one that justifies the package: a Nova text/heading field on a detail view that shows copy with tokens in it, resolved per record at render time. For example a Text field whose value is Welcome back, {{user.name}} — you have {{user.open_tickets}} open tickets, where {{...}} is swapped for the current model's attributes when the field is displayed. Register the placeholders once in a service provider's boot(), then let resource fields carry the token string as plain data.
Start with one placeholder, one field, one resource, and confirm the substitution happens in the Detail view before wiring it into Index or lenses.
Register placeholders centrally, never inline. Keep all token definitions in a dedicated provider (e.g. DynamicViewServiceProvider) so the mapping from token to closure is one searchable file:
public function boot(): void
{
DynamicViews::placeholder('user.name', fn ($model) => $model->user?->name ?? '');
DynamicViews::placeholder('user.open_tickets', fn ($model) => $model->user?->openTickets()->count() ?? 0);
}
The closure should receive the current model/resource context — that is the whole point of "dynamic". If the package's resolver only takes a string, wrap it so the model is captured from request()->route('resource') / the resource instance.
Use it where Nova fields are read-only. The natural fit is Text, Heading, Badge, Stack inner text on Detail and Index — places where you want presentational copy that varies per row. Do not use it to populate editable form values; a token string leaking into an editable Text field will get saved back to the database on the next update and corrupt the record.
Compose with displayUsing rather than replacing it. If a field already uses a closure, resolve placeholders inside that closure so ordering is explicit:
Text::make('Summary')
->displayUsing(fn ($value, $resource) => DynamicViews::render($value, $resource))
->asHtml(),
Per-resource registration for tenant/branding variation. If different resources need different token sets, register in the resource's boot()/fields() lifecycle or key the registry by resource class, rather than one global map that grows unbounded.
Keep resolution cheap. Placeholders that hit the database (counts, latest activity) run once per rendered field. Batch them: resolve all tokens for a model in a single pass and memoise on the model instance, otherwise an index of 50 rows triggers 50+ queries per token.
Test at the resolver level, not through Nova. A single feature test that boots the provider and asserts render('Hello {{user.name}}', $model) returns the expected string will catch 90% of regressions — Nova's HTTP layer adds noise and is version-sensitive.
It is archived — pin and fork. Add an exact constraint ("bernhardh/nova-dynamic-views": "1.0.0" or a commit hash), and vendor a fork under your org if you intend to keep using it past the next Nova major. Composer's dev-main on an abandoned repo means an unreviewed force-push can land in production.
Nova version drift is the real risk. A 2022 package predates Nova 4/5 field internals; Text/Heading rendering and the displayUsing signature changed. Check composer.json constraints and smoke-test Detail, Index, and lens views separately — a field can render fine in one and throw in another.
Unknown tokens fail silently. Most placeholder libraries leave {{unknown}} untouched. That is convenient until a typo ships to production looking like literal braces in the admin UI. Wrap render() in your own helper that logs unresolved tokens (Log::warning with the token and resource class) so misses surface.
Escape user-controlled values. Placeholder results are interpolated into field output; if ->asHtml() is on, a model attribute containing <script> becomes an admin-panel XSS vector. Escape in the closure (e(...)) unless you have deliberately opted into HTML.
Nested / re-entrant tokens are undefined behaviour. Do not register a placeholder whose output contains another token, or resolution order becomes a silent source of inconsistency. Resolve in a single pass, or document the order explicitly if the package supports multiple passes.
Caching can freeze dynamic output. If you cache rendered field values (response cache, Nova's own field caching, or an ->onlyOnDetail() value memoised wrongly), "dynamic" becomes "stale". Resolve per request; cache the underlying counts, not the rendered string.
Debugging shortcut: dump the resolver input, not the output. Confirm the closure receives the model you expect — a null model from a mis-scoped route is the most common cause of "placeholders render empty in lenses but work in detail".
Extension point: rather than editing vendor code, decorate the package's manager binding in your own provider ($this->app->extend(...)) so you can add strict resolution, memoisation, and logging without forking — and keep that decoration in one file for the day you do have to fork.
How can I help you explore Laravel packages today?