memran/marwa-view

A modern Twig-powered view layer for PHP 8.2+ with fragment caching, theme inheritance, and framework-agnostic DX.

Maintainers

Package info

github.com/memran/marwa-view

pkg:composer/memran/marwa-view

Statistics

Installs: 132

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-02 11:59 UTC

This package is auto-updated.

Last update: 2026-04-02 12:15:51 UTC


README

Packagist Version Packagist Downloads CI PHP 8.2+ PHPStan 2.x License: MIT

Marwa\View is a framework-agnostic view layer for PHP 8.2+ built on Twig. It gives application code a small public API, optional PSR-16 fragment caching, theme inheritance, and extension points without forcing the rest of the app to depend directly on Twig internals.

Documentation is split into focused guides under docs/README.md. Start there to browse tutorials, API references, themes, extensions, examples, and development notes.

Features

  • Small public API centered on View, ViewConfig, and ViewInterface
  • Twig-powered rendering with strict variables and auto-reload in debug mode
  • Shared view data through share()
  • Nested partial rendering through view() inside templates
  • Namespaced views for modular template directories like @Blog/post/index
  • Layout stack helpers through push(), prepend(), and stack()
  • PSR-16 fragment caching through fragment()
  • Optional runtime theme switching with ThemeBuilder
  • Theme inheritance for templates and theme-specific asset URLs
  • Optional extensions for assets, URLs, text helpers, dates, translations, HTML attributes, Alpine.js directives, JSON output, and money formatting
  • PHPUnit, PHPStan 2.x, PHP-CS-Fixer, Composer scripts, and GitHub Actions CI

Requirements

  • PHP 8.2+
  • Composer 2

Installation

composer require memran/marwa-view

For package development:

composer install

Documentation

Tutorial

1. Create a views directory

project/
  views/
    home/
      index.twig
  storage/
    views/

2. Configure the renderer

<?php

declare(strict_types=1);

use Marwa\View\View;
use Marwa\View\ViewConfig;

require __DIR__ . '/vendor/autoload.php';

$config = new ViewConfig(
    viewsPath: __DIR__ . '/views',
    cachePath: __DIR__ . '/storage/views',
    debug: true,
);

$view = new View($config);

3. Share global view data

$view->share('appName', 'Marwa Demo');
$view->share('auth', [
    'id' => 42,
    'name' => 'Emran',
]);

4. Render a template

echo $view->render('home/index', [
    'title' => 'Dashboard',
]);

Logical template names are slash-based. home/index resolves to home/index.twig.

5. Use the data in Twig

{# views/home/index.twig #}
<h1>{{ title }}</h1>
<p>Welcome to {{ appName }}</p>
<p>Signed in as {{ auth.name }}</p>

Public API

Marwa\View\ViewConfig

Creates the renderer configuration and validates paths eagerly.

$config = new ViewConfig(
    viewsPath: __DIR__ . '/views',
    cachePath: __DIR__ . '/storage/views',
    debug: false,
    fragmentCache: $cache, // optional PSR-16 cache
);

Constructor arguments:

  • viewsPath: base directory that contains .twig templates
  • cachePath: writable Twig compilation cache directory
  • debug: enables Twig debug-friendly behavior
  • fragmentCache: optional PSR-16 cache used by fragment()

Marwa\View\View

Main rendering service.

$view = new View($config, extensions: [
    // optional Twig extensions
]);

Public methods:

  • render(string $template, array $data = []): string
  • display(string $template, array $data = []): void
  • share(string $name, mixed $value): void
  • clearCache(): void
  • addNamespace(string $namespace, string $path): void
  • pushToStack(string $stack, string $content): void
  • prependToStack(string $stack, string $content): void
  • renderStack(string $stack, string $glue = "\n"): string
  • fragment(string $key, int $ttl, callable|array $producer): string
  • addExtension(AbstractExtension $extension): void
  • getThemeBuilder(): ?ThemeBuilder

Marwa\View\ViewInterface

Stable contract for application code that only needs rendering, shared data, and cache clearing.

Theme API

The theming system lives under Marwa\View\Theme:

  • ThemeConfig: immutable theme definition
  • ThemeRegistry: collection of registered themes
  • ThemeResolver: resolves template and asset lookups through the inheritance chain
  • ThemeBuilder: runtime facade used by the view layer
  • ThemeBootstrap: convenience loader that builds themes from a directory structure

Translation API

The translation helpers live under Marwa\View\Translate:

  • TranslatorInterface
  • ArrayTranslator

Usage Guide

Basic rendering

echo $view->render('dashboard', [
    'user' => $user,
    'metrics' => $metrics,
]);

Shared data

$view->share('csrf', 'token-value');
$view->share('locale', 'en');

Display directly

$view->display('pages/about');

Nested partials in Twig

{{ view('components/card', { title: 'Status', value: 'Healthy' })|raw }}

Translation

Create language files:

<?php

return [
    'welcome' => [
        'title' => 'Welcome back, :name!',
    ],
    'cart' => [
        'items' => [
            'one' => ':count item',
            'other' => ':count items',
        ],
    ],
];

Register the translator:

use Marwa\View\Extension\TranslateExtension;
use Marwa\View\Translate\ArrayTranslator;

$translator = new ArrayTranslator('en', __DIR__ . '/lang');

$view = new View($config, [
    new TranslateExtension($translator),
]);

Use it in Twig:

<h1>{{ t('welcome.title', { name: auth.name }) }}</h1>
<p>{{ tc('cart.items', cartCount) }}</p>

Rendered output:

<h1>Welcome back, Avery!</h1>
<p>2 items</p>

Namespaced views

Register a module namespace:

$config = new ViewConfig(
    viewsPath: __DIR__ . '/views',
    cachePath: __DIR__ . '/storage/views',
    debug: true,
    namespaces: [
        'Blog' => __DIR__ . '/modules/Blog/views',
    ],
);

Render namespaced templates:

echo $view->render('@Blog/post/show', ['post' => $post]);

From Twig:

{{ view('@Blog/teaser', { appName: appName })|raw }}

For full module-specific template setup and controller examples, see docs/modules.md.

Layout stacks

From PHP:

$view->pushToStack('scripts', '<script src="/app.js"></script>');
$view->prependToStack('head', '<meta name="robots" content="noindex">');

From Twig:

{% set pageScript %}
  <script src="/dashboard.js"></script>
{% endset %}
{{ push('scripts', pageScript) }}

Render a stack in the layout:

{{ stack('head')|raw }}
{{ stack('scripts')|raw }}

Fragment caching

From Twig:

{{ fragment('sidebar', 300, {
    template: 'partials/sidebar',
    data: { user: auth }
})|raw }}

From PHP:

$html = $view->fragment('stats', 60, fn (): string => '<strong>Cached</strong>');

Clearing caches

$view->clearCache();

This clears both the PSR-16 fragment cache and compiled Twig cache files for the configured renderer.

Extensions

The package ships with optional Twig extensions:

  • AssetExtension
  • UrlExtension
  • TextExtension
  • DateExtension
  • TranslateExtension
  • HtmlExtension
  • AlpineExtension
  • JsonExtension
  • MoneyExtension
  • NumberExtension
  • MetaStackExtension
  • IconExtension
  • SeoExtension
  • ListExtension
  • ImageExtension
  • StringPresentationExtension
  • StatusExtension

Example:

use Marwa\View\Extension\AlpineExtension;
use Marwa\View\Extension\AssetExtension;
use Marwa\View\Extension\DateExtension;
use Marwa\View\Extension\HtmlExtension;
use Marwa\View\Extension\IconExtension;
use Marwa\View\Extension\ImageExtension;
use Marwa\View\Extension\JsonExtension;
use Marwa\View\Extension\ListExtension;
use Marwa\View\Extension\MetaStackExtension;
use Marwa\View\Extension\MoneyExtension;
use Marwa\View\Extension\NumberExtension;
use Marwa\View\Extension\SeoExtension;
use Marwa\View\Extension\StatusExtension;
use Marwa\View\Extension\StringPresentationExtension;
use Marwa\View\Extension\TextExtension;
use Marwa\View\Extension\TranslateExtension;
use Marwa\View\Extension\UrlExtension;
use Marwa\View\Translate\ArrayTranslator;

$translator = new ArrayTranslator('en', __DIR__ . '/lang');

$view = new View($config, [
    new AssetExtension('/static', '1.0.0'),
    new AlpineExtension(),
    new TextExtension(),
    new DateExtension(),
    new HtmlExtension(),
    new ImageExtension(),
    new JsonExtension(),
    new ListExtension(),
    new MoneyExtension(),
    new NumberExtension(),
    new StatusExtension(),
    new StringPresentationExtension(),
    new UrlExtension('https://demo.test'),
    new TranslateExtension($translator),
]);

$view->addExtension(new MetaStackExtension($view));
$view->addExtension(new SeoExtension($view));
$view->addExtension(new IconExtension([
    'spark' => '<svg viewBox="0 0 24 24"><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6Z"/></svg>',
]));

Useful helpers from the new extensions:

<div {{ ui().data({ open: false }) }}>
  <button {{ ui().click('open = !open') }}>Toggle</button>
</div>

<button {{ html_attrs({
    type: 'button',
    class: ['btn', 'btn-primary'],
    disabled: isDisabled
}) }}>
    Save
</button>

{{ money(1250.5, 'USD') }}
{{ json({ app: appName, locale: locale }) }}
{{ json_script('page-state', { user: auth }) }}
{{ compact_number(18420) }}
{{ file_size(5368709120) }}
{{ icon('spark', { class: 'h-4 w-4' }) }}
{{ push_meta('description', 'Dashboard page') }}
{{ meta_description('Dashboard page') }}
{{ oxford_join(['themes', 'stacks', 'fragments']) }}
<img {{ image_attrs('/images/panel.svg', 'Panel preview', { loading: 'lazy' }) }}>
{{ initials('Riley Harper') }}
{{ headline('framework_style_templates') }}
{{ status_label('pending') }}
{{ status_classes('active') }}

class_names() accepts strings, flat string lists, or { className: condition } maps. html_attrs() supports scalar attributes, boolean attributes, and class values built from the same shapes. ui() exposes the optional Alpine bridge so templates can render x-data, x-on:*, x-show, x-bind:*, and related directives without handwritten attribute strings.

Themes

Themes are optional. When enabled, Twig still supports extends, include, and other loader-based features while the active theme changes at runtime.

Theme directory structure

themes/
  default/
    manifest.php
    views/
      layout.twig
      home/
        index.twig
    assets/
      css/
        app.css
  dark/
    manifest.php
    views/
      layout.twig
  tenantA/
    manifest.php
    views/
      home/
        index.twig

Theme manifest

<?php

declare(strict_types=1);

return [
    'name' => 'tenantA',
    'parent' => 'dark',
    'assets_url' => '/themes/tenantA',
    'meta' => [
        'label' => 'Tenant A',
        'description' => 'Tenant-specific branding layered on top of the dark base theme.',
        'version' => '1.0.0',
        'author' => 'Example Studio',
        'preview_image' => '/themes/tenantA/images/logo-tenantA.svg',
        'tags' => ['tenant', 'green-accent'],
    ],
];

Supported metadata keys:

  • label
  • description
  • version
  • author
  • preview_image
  • tags

Bootstrap themes from a directory

use Marwa\View\Theme\ThemeBootstrap;
use Marwa\View\View;
use Marwa\View\ViewConfig;

$themeBuilder = ThemeBootstrap::initFromDirectory(
    themesBaseDir: __DIR__ . '/themes',
    defaultTheme: 'default',
);

$themeBuilder->useTheme('tenantA');

$view = new View(
    config: new ViewConfig(
        viewsPath: __DIR__ . '/views',
        cachePath: __DIR__ . '/storage/views',
        debug: true,
    ),
    themeBuilder: $themeBuilder,
);

echo $view->render('home/index');

Access theme data in Twig

Available globals:

  • _theme_name
  • _theme_chain
  • _theme_meta
  • _theme_selected
  • _theme_selected_meta
  • _theme_previewing
  • _theme_preview
  • _theme_available
  • _theme_catalog

Helper function:

<link rel="stylesheet" href="{{ theme_asset('css/app.css') }}">

Translation

ArrayTranslator loads locale files from a directory of PHP arrays.

use Marwa\View\Extension\TranslateExtension;
use Marwa\View\Translate\ArrayTranslator;

$translator = new ArrayTranslator('en', __DIR__ . '/lang');

$view = new View($config, [
    new TranslateExtension($translator),
]);

Locale file example:

<?php

declare(strict_types=1);

return [
    'welcome.title' => 'Welcome, :name!',
    'cart.items' => [
        'one' => ':count item',
        'other' => ':count items',
    ],
];

Twig usage:

{{ t('welcome.title', { name: user.name }) }}
{{ tc('cart.items', cartCount) }}

Example Files

The repository includes runnable examples:

1.0 Readiness Checklist

  • Implemented and tested small public API around View and ViewConfig
  • Implemented and tested namespaced views
  • Implemented and tested stack helpers for layout injection
  • Implemented and tested theming, preview mode, and manifest metadata
  • Added PHPUnit, PHPStan 2.x, PHP-CS-Fixer, Composer scripts, and CI
  • Locked Composer platform to PHP 8.2 for reproducible dependency resolution
  • Added public API regression coverage for documented package surface

Recommended before tagging 1.0.0:

  • publish a changelog policy and semantic versioning policy
  • add upgrade notes when public behavior changes
  • decide whether future stack/theme extensions belong in-core or in companion packages

Feature-Claim Audit

Implemented and documented:

  • framework-facing View API
  • Twig-hidden rendering boundary
  • fragment caching
  • shared globals
  • namespaced views
  • stack system
  • translation helpers with pluralization
  • themes with inheritance, switching, preview mode, and manifest metadata

Explicitly not claimed:

  • routing
  • controllers
  • HTTP/session abstraction
  • persistence of user or tenant theme preferences
  • framework-specific service container integration beyond examples

Quality Tooling

Available Composer scripts:

  • composer test
  • composer test:coverage
  • composer analyse
  • composer lint
  • composer fix
  • composer ci

Configuration files:

Production Notes

  • Set debug to false outside development.
  • Use a real PSR-16 cache backend in production.
  • Keep Twig cache directories writable but outside the public web root.
  • Do not pass raw user input directly to template names or theme names.
  • Treat theme manifests and translation files as trusted application code.

Contributing

  1. Run composer install.
  2. Make focused changes with tests.
  3. Run composer ci.
  4. Open a pull request with the problem, approach, and verification summary.

License

MIT