PHPackages                             toppy/twig-view-model - 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. [Templating &amp; Views](/categories/templating)
4. /
5. toppy/twig-view-model

ActiveLibrary[Templating &amp; Views](/categories/templating)

toppy/twig-view-model
=====================

Twig integration for async view models - view() and pre\_load\_view() functions

v0.7.5(2w ago)081↓87.5%1proprietaryPHPPHP &gt;=8.4

Since Jan 23Pushed 2w agoCompare

[ Source](https://github.com/toppynl/twig-view-model)[ Packagist](https://packagist.org/packages/toppy/twig-view-model)[ RSS](/packages/toppy-twig-view-model/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (15)Versions (17)Used By (1)

Twig View Model
===============

[](#twig-view-model)

> **Read-Only Repository**This is a read-only subtree split from the main repository. Please submit issues and pull requests to [toppynl/symfony-astro](https://github.com/toppynl/symfony-astro).

Twig integration for async view models - provides the `view()` function for accessing resolved data in templates. This package bridges the `toppy/async-view-model` core with Twig's template engine through compile-time AST scanning and runtime data access.

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

[](#installation)

```
composer require toppy/twig-view-model
```

Requirements
------------

[](#requirements)

- PHP 8.4+
- Twig 3.23+
- [toppy/async-view-model](https://github.com/toppynl/symfony-astro) (automatically installed as dependency)

Quick Start
-----------

[](#quick-start)

```
{# Template declares which data it needs, then accesses resolved results #}
{% set [product, error] = view('App\\ViewModel\\ProductViewModel') %}

{% if error %}
    {{ error.message }}
{% elseif product %}
    {{ product.name }}
    {{ product.description }}
{% endif %}
```

Register the extension with Twig:

```
use Toppy\TwigViewModel\Twig\ViewExtension;
use Toppy\TwigViewModel\Twig\Runtime\ViewModelRuntime;

$twig->addExtension(new ViewExtension($twig->getLoader()));
$twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
    public function load(string $class): ?object {
        if ($class === ViewModelRuntime::class) {
            return new ViewModelRuntime($viewModelManager);
        }
        return null;
    }
});
```

Architecture
------------

[](#architecture)

### Key Classes

[](#key-classes)

ClassPurpose`ViewExtension`Twig extension that registers the `view()` function and AST visitor`ViewDiscoveryVisitor`Node visitor that scans templates at compile-time to discover all `view()` calls`ViewModelRuntime`Runtime handler for the `view()` function, interfaces with `ViewModelManagerInterface``DoPreloadMethodNode`Twig node that generates a `doPreload()` method in compiled templates`ViewModelError`Template-facing error representation with structured codes for error handling### Directory Structure

[](#directory-structure)

```
TwigViewModel/
├── Twig/
│   ├── ViewExtension.php           # Twig extension registration
│   ├── Runtime/
│   │   └── ViewModelRuntime.php    # view() function implementation
│   ├── NodeVisitor/
│   │   └── ViewDiscoveryVisitor.php # Compile-time AST scanning
│   └── Node/
│       └── DoPreloadMethodNode.php  # Generates doPreload() method
├── ViewModelError.php               # Error representation for templates
├── Tests/
│   └── Unit/                        # PHPUnit test suite
├── composer.json
└── phpunit.xml

```

Usage
-----

[](#usage)

### The view() Function

[](#the-view-function)

The `view()` function retrieves resolved data from a preloaded view model. It returns an indexed array for sequence destructuring:

```
{% set [data, error] = view('App\\ViewModel\\StockViewModel') %}
```

The return value is always a 2-element array:

- `data` - The resolved data object (or `null` if unavailable)
- `error` - A `ViewModelError` object (or `null` if successful)

#### Handling Different States

[](#handling-different-states)

```
{% set [stock, error] = view('App\\ViewModel\\StockViewModel') %}

{# Error state - resolution failed #}
{% if error %}
    {% if error.code == 'TIMEOUT' %}
        Stock information temporarily unavailable
    {% elseif error.code == 'NOT_FOUND' %}
        Product not found
    {% else %}
        Error: {{ error.message }}
    {% endif %}

{# Success state - data available #}
{% elseif stock %}
    {{ stock.quantity }} in stock

{# No data state - view model returned nothing #}
{% else %}
    Stock information not available
{% endif %}
```

#### Error Codes

[](#error-codes)

The `ViewModelError` maps exceptions to semantic codes:

CodeException TypeDescription`NOT_FOUND``NotFoundHttpException`Resource doesn't exist`FORBIDDEN``AccessDeniedHttpException`Access denied`UNAUTHORIZED``UnauthorizedHttpException`Authentication required`SERVICE_UNAVAILABLE``ServiceUnavailableHttpException`Backend service down`RATE_LIMITED``TooManyRequestsHttpException`Rate limit exceeded`TIMEOUT``TimeoutException`Request timed out`RESOLUTION_FAILED``ViewModelResolutionException`Generic resolution failure`UNKNOWN`Any other exceptionUnexpected error### AST Discovery

[](#ast-discovery)

The `ViewDiscoveryVisitor` performs compile-time scanning of Twig templates to discover all view model dependencies. This enables the preloading system to know which view models a template needs before rendering begins.

#### How It Works

[](#how-it-works)

1. **Template Compilation**: When Twig compiles a template, the visitor scans the AST
2. **view() Detection**: Finds all `view('ClassName')` function calls
3. **Validation**: Verifies each class exists and implements `AsyncViewModel`
4. **Include Scanning**: Recursively scans static `{% include %}` directives
5. **Method Injection**: Generates a `doPreload()` method in the compiled template class

#### Generated doPreload() Method

[](#generated-dopreload-method)

Each compiled template with `view()` calls gets a `doPreload()` method:

```
// Auto-generated in compiled template
public function doPreload(): array
{
    $classes = [
        'App\\ViewModel\\ProductViewModel',
        'App\\ViewModel\\StockViewModel',
    ];

    // Chain to parent template if exists
    $parentName = $this->doGetParent([]);
    if ($parentName !== false) {
        $parent = $this->load($parentName, 0)->unwrap();
        if (method_exists($parent, 'doPreload')) {
            $classes = array_merge($parent->doPreload(), $classes);
        }
    }

    return array_values(array_unique($classes));
}
```

This method:

- Returns all view model classes discovered in the template
- Chains to parent templates (for `{% extends %}` hierarchies)
- Deduplicates results across the inheritance chain

#### Compile-Time Validation

[](#compile-time-validation)

The visitor validates at compile time:

```
{# Error: Class does not exist #}
{% set [data, error] = view('App\\NonExistent\\ViewModel') %}
{# Throws: View model class "App\NonExistent\ViewModel" does not exist. #}

{# Error: Class doesn't implement AsyncViewModel #}
{% set [data, error] = view('App\\Entity\\Product') %}
{# Throws: Class "App\Entity\Product" must implement AsyncViewModel. #}
```

#### Include Scanning

[](#include-scanning)

Static includes are recursively scanned:

```
{# main.html.twig #}
{% set [product, error] = view('App\\ViewModel\\ProductViewModel') %}
{% include 'partials/stock.html.twig' %}

{# partials/stock.html.twig #}
{% set [stock, error] = view('App\\ViewModel\\StockViewModel') %}
```

The `doPreload()` method for `main.html.twig` will include both `ProductViewModel` and `StockViewModel`.

**Note**: Dynamic includes (`{% include variable %}`) cannot be scanned at compile-time.

Integration
-----------

[](#integration)

This package is Layer 1 in the Toppy Stack architecture:

```
┌─────────────────────────────────────┐
│  symfony-async-twig-bundle (L3)     │  Symfony integration
└──────────────────┬──────────────────┘
                   │
      ┌────────────┴────────────┐
      ▼                         ▼
┌─────────────┐      ┌──────────────────┐
│ twig-prerender (L2)│  twig-streaming   │
└─────────────┘      └──────────────────┘
                   │
         ┌─────────┴─────────┐
         ▼                   ▼
┌─────────────────┐  ┌────────────────────┐
│ twig-view-model │  │                    │
│      (L1)       │  │                    │
└────────┬────────┘  │                    │
         │           │                    │
         └─────┬─────┘                    │
               ▼                          │
┌─────────────────────────────────────────┘
│        async-view-model (L0)
│     Framework-agnostic core
└─────────────────────────────────────────

```

### Dependency on async-view-model

[](#dependency-on-async-view-model)

This package depends on `toppy/async-view-model` for:

- `AsyncViewModel` interface - Contract for async data fetching
- `ViewModelManagerInterface` - Orchestrates view model resolution
- `NoDataException` - Signals no data available (not an error)
- `ViewModelNotPreloadedException` - Developer error: view model wasn't preloaded
- `ViewModelResolutionException` - Resolution failed with error details

Testing
-------

[](#testing)

Run the test suite:

```
cd src/Toppy/Component/TwigViewModel
./vendor/bin/phpunit
```

Or from the monorepo root:

```
make demo-shell
cd /app/src/Toppy/Component/TwigViewModel && ./vendor/bin/phpunit
```

### Test Coverage

[](#test-coverage)

- `ViewModelRuntimeTest` - Tests the `view()` function behavior
- `ViewModelErrorTest` - Tests error code mapping and serialization

License
-------

[](#license)

Proprietary - See [LICENSE](LICENSE) file for details.

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance97

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 97.2% 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

Every ~9 days

Recently: every ~2 days

Total

16

Last Release

16d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/239d9adcbadfaac1e3ce531c1b81d87e378c3395b9c10bef5bfddb1637c07c9d?d=identicon)[Swahjak](/maintainers/Swahjak)

---

Top Contributors

[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (35 commits)")[![Swahjak](https://avatars.githubusercontent.com/u/4386577?v=4)](https://github.com/Swahjak "Swahjak (1 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/toppy-twig-view-model/health.svg)

```
[![Health](https://phpackages.com/badges/toppy-twig-view-model/health.svg)](https://phpackages.com/packages/toppy-twig-view-model)
```

###  Alternatives

[craftcms/cms

Craft CMS

3.6k3.6M3.1k](/packages/craftcms-cms)[symfony/ux-twig-component

Twig components for Symfony

22018.6M355](/packages/symfony-ux-twig-component)[symfony/ux-live-component

Live components for Symfony

1647.0M128](/packages/symfony-ux-live-component)

PHPackages © 2026

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