PHPackages                             artis-auxilium/laravel-lazy-view-models - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. artis-auxilium/laravel-lazy-view-models

ActiveLibrary

artis-auxilium/laravel-lazy-view-models
=======================================

Lazy load data in blade view

v1.0.0(today)00MITPHPPHP ^8.2CI passing

Since Aug 1Pushed todayCompare

[ Source](https://github.com/artis-auxilium/laravel-lazy-view-models)[ Packagist](https://packagist.org/packages/artis-auxilium/laravel-lazy-view-models)[ Docs](https://github.com/artis-auxilium/laravel-lazy-view-models)[ RSS](/packages/artis-auxilium-laravel-lazy-view-models/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (9)Versions (2)Used By (0)

Laravel Lazy View Models
========================

[](#laravel-lazy-view-models)

Lazy, reflection-based view models for Laravel Blade templates.

`laravel-lazy-view-models` lets you write view models where every public method behaves like a **property**: it's computed lazily, on first access, and that computation happens **at most once**, no matter how many times or from how many places the value is used.

Why
---

[](#why)

View models are a great way to keep business logic out of Blade templates, but a naive implementation calls every method up front to build an array of data, even for values the view never touches (conditionally hidden blocks, unused partials, etc.), and recomputes a value every time it's read. This package solves both problems: methods are only invoked when actually needed, and only ever once.

Installation
------------

[](#installation)

```
composer require artis-auxilium/laravel-lazy-view-models
```

Requires PHP 8.2+ and Laravel (Illuminate Support/Contracts). Tested against PHP 8.2, 8.3, 8.4, and 8.5 in CI.

Basic usage
-----------

[](#basic-usage)

Extend the abstract `ViewModel` class and expose data via **public properties** or **public methods**:

```
use ArtisAuxilium\LaravelLazyViewModels\ViewModel;

final class InvoiceViewModel extends ViewModel
{
    public function __construct(
        public readonly Invoice $invoice,
    ) {}

    public function total(): string
    {
        return number_format($this->invoice->total, 2) . ' €';
    }

    public function customerName(): string
    {
        return $this->invoice->customer->name;
    }
}
```

The key idea: even though `total()` and `customerName()` are declared as methods, everywhere else in the application (in Blade, in other view model methods, from outside the class) they behave as **properties** — `$viewModel->total`, not `$viewModel->total()`. That property-style access is what triggers the computation, and only the first access does any work; any later access to the same property, from anywhere, reuses the same result.

Since `ViewModel` implements `Arrayable`, pass the instance itself as the view data — Laravel converts it to an array, and every exposed property becomes its own top-level Blade variable:

```
return view('invoices.show', new InvoiceViewModel($invoice));
```

```
{{ $customerName }}
Total: {{ $total }}
```

`total` and `customerName` are only computed if and when the template actually reaches these lines.

### Callables

[](#callables)

If a property's value is itself a `callable`, invoke it in the template:

```
final class ButtonViewModel extends ViewModel
{
    public function label(): callable
    {
        return fn () => strtoupper($this->text);
    }
}
```

```
{{ $label() }}
```

### Methods with parameters

[](#methods-with-parameters)

A method that declares parameters can't be auto-resolved as a property — there's no way to know what arguments to call it with. Instead, it's exposed as directly invokable, and called in Blade with whatever arguments the template wants to pass:

```
final class InvoiceViewModel extends ViewModel
{
    public function formattedTotal(string $currency): string
    {
        return number_format($this->invoice->total, 2) . " {$currency}";
    }
}
```

```
{{ $formattedTotal('€') }}
```

Unlike a parameterless property, this isn't memoized — the underlying method runs on every call, since the result can differ depending on the arguments. `#[IsHtml]` still applies normally here: the return value of the call is escaped or not exactly as it would be for a parameterless property.

### Arrays and iteration

[](#arrays-and-iteration)

Arrays and traversables work transparently with `@foreach`:

```
final class ListViewModel extends ViewModel
{
    /** @return object[] */
    public function items(): array
    {
        return [(object) ['value' => 'a'], (object) ['value' => 'b']];
    }
}
```

```
@foreach($items as $item) {{ $item->value }} @endforeach
```

### Values shared between methods

[](#values-shared-between-methods)

Because every exposed method behaves like a property from anywhere in the class, one method can depend on another simply by reading it as a property — and the underlying computation is still only ever run once, whether it ends up being triggered by the dependent method, by Blade, or by both:

```
final class ExampleViewModel extends ViewModel
{
    public function base(): string
    {
        return 'result'; // expensive computation, e.g. a DB query
    }

    public function dependent(): string
    {
        return $this->base . '_dep'; // property access, not base()
    }
}
```

Whether `base` gets resolved first because the template reads `{{ $base }}`, or because `dependent` reads it internally, the second read of `base` — wherever it comes from — reuses the already-computed result instead of running `base()` again.

`ViewValue`: the lazy property wrapper
--------------------------------------

[](#viewvalue-the-lazy-property-wrapper)

Every non-ignored public method is wrapped in a `ViewValue` when the view model is converted to an array (which Laravel does automatically when a view model is passed to a view). `ViewValue` is what makes property-style access to a method's result work:

- Resolves the underlying method **once**, on first access, then reuses the result for every later access.
- Implements `Stringable` — casts to string via `(string) $value`, provided the resolved value is a scalar or itself `Stringable`.
- Implements `ArrayAccess` — usable as `$value['key']` if the resolved value is an array or `ArrayAccess`.
- Implements `Countable` — usable with `count($value)` if the resolved value is an array or `Countable`.
- Implements `IteratorAggregate` — usable in `@foreach` if the resolved value is `Traversable` or an array.
- Implements `DeferringDisplayableValue` — Blade's `{{ }}` echoing correctly handles `Htmlable` results without double-escaping.
- Is invokable — `$value()` calls the resolved value if it's callable.

Additional helper methods on `ViewValue`:

MethodDescription`value(): mixed`Force resolution and return the raw underlying value.`empty(): bool``empty($resolved)`.`notEmpty(): bool`Inverse of `empty()`.`isset(): bool``isset($resolved)`.> **Note:** a wrapped method returning a `Generator` throws an `IterableException` — generators can only be consumed once, which is incompatible with property-style, potentially-repeated access. Return a `LazyCollection` (or any other `Traversable`/array) instead.

Documenting Blade views
-----------------------

[](#documenting-blade-views)

Because a view model's variables reach Blade already resolved by name (not as `$viewModel->property`), the IDE loses the link back to the class that defines them, and can't tell you their real type. A useful convention is to document that link at the top of the Blade file with a `@php` block:

```
@php
    /** @see InvoiceViewModel */
    /** @var string $customerName */
    /** @var ViewValue $total */
@endphp
```

- `@see InvoiceViewModel` links the template back to the view model class, so your IDE can jump to it.
- Public **properties** (like `customerName` if it were a public property on the view model) reach Blade with their actual type — document them as `@var Type $name`.
- Values coming from **methods** (like `total`) reach Blade as `ViewValue`instances, not as a raw `T` — document them as `@var ViewValue $name`, with `T` being the method's real return type. `ViewValue` proxies string casting, array access, iteration, etc. to the resolved value, so it behaves like `T` at runtime, but typing it precisely as `ViewValue`keeps IDE autocompletion accurate.

Attributes
----------

[](#attributes)

### `#[Ignore]`

[](#ignore)

Exclude a public property or method from the view model's exposed data — it won't be turned into a property and won't reach the view. Useful for public helper methods but shouldn't be exposed to Blade:

```
use ArtisAuxilium\LaravelLazyViewModels\Attribute\Ignore;

final class InvoiceViewModel extends ViewModel
{
    public function total(): string
    {
        return $this->formatAmount($this->invoice->total);
    }

    #[Ignore]
    public function formatAmount(float $amount): string
    {
        return number_format($amount, 2) . ' €';
    }
}
```

Magic methods (`__construct`, `__toString`, `__invoke`, ...) are always excluded automatically — there's no need to mark them with `#[Ignore]`.

### `#[IsHtml]`

[](#ishtml)

Mark a method's result as pre-escaped HTML. The resolved value is wrapped in an `Illuminate\Support\HtmlString`, so Blade's `{{ }}` will output it unescaped:

```
use ArtisAuxilium\LaravelLazyViewModels\Attribute\IsHtml;

final class ArticleViewModel extends ViewModel
{
    #[IsHtml]
    public function renderedBody(): string
    {
        return Str::markdown($this->article->body);
    }
}
```

```
{{-- Outputs raw HTML, not escaped --}}
{{ $renderedBody }}
```

If the resolved value isn't a scalar or `Stringable`, the HTML-wrapped result falls back to an empty string.

Exceptions
----------

[](#exceptions)

All exceptions live under `ArtisAuxilium\LaravelLazyViewModels\Exception` and are thrown when a property's value is used in a way that's incompatible with what it actually resolves to:

ExceptionThrown when`StringException`Casting to string (`(string) $value` / `{{ $value }}`) but the resolved value isn't scalar or `Stringable`.`ArrayAccessException`Using array access (`$value['key']`) but the resolved value isn't an array or `ArrayAccess`.`CountableException`Calling `count($value)` but the resolved value isn't an array or `Countable`.`IterableException`Iterating (`@foreach`) a non-iterable value, or when the resolved value is a `Generator`.> **Note:** these exceptions cover misuse of a resolved value's *type*. For [methods with parameters](#methods-with-parameters), calling the exposed closure with missing or invalid arguments raises a native PHP error (`ArgumentCountError`, `TypeError`, ...), not one of the exceptions above.

Testing &amp; quality
---------------------

[](#testing--quality)

```
composer test
```

The suite maintains 100% line/method coverage and a 100% Mutation Score Indicator (via [Infection](https://infection.github.io/)) — the CI fails if either drops. A few notes for contributors:

- **Static analysis**: [PHPStan](https://phpstan.org/) at level 10 (the strictest), via [Larastan](https://github.com/larastan/larastan), run against both `src` and `tests`.
- **Mutation testing**: Infection is configured with `minMsi: 100` and `minCoveredMsi: 100` — any mutant that survives, or any line not covered by a test that kills its mutants, fails the build.
- **PHPUnit** runs in strict mode (`failOnRisky`, `failOnWarning`, `requireCoverageMetadata`) — tests must explicitly declare what they cover, and any deprecation, warning, or risky test fails the suite.

License
-------

[](#license)

MIT.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6b3f65afa37cecf8667869738f3cc70656cd4c881721b8b6ba1fc3da155e7d83?d=identicon)[Dev2a](/maintainers/Dev2a)

---

Top Contributors

[![d3v2a](https://avatars.githubusercontent.com/u/1815655?v=4)](https://github.com/d3v2a "d3v2a (25 commits)")

---

Tags

blade-templatelaravellazy

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/artis-auxilium-laravel-lazy-view-models/health.svg)

```
[![Health](https://phpackages.com/badges/artis-auxilium-laravel-lazy-view-models/health.svg)](https://phpackages.com/packages/artis-auxilium-laravel-lazy-view-models)
```

###  Alternatives

[craftcms/cms

Craft CMS

3.6k3.7M3.3k](/packages/craftcms-cms)[psalm/plugin-laravel

Psalm plugin for Laravel

3355.4M352](/packages/psalm-plugin-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
