iviphp/view

View discovery and PHP template rendering for the IviPHP ecosystem.

Maintainers

Package info

github.com/iviphp/view

pkg:composer/iviphp/view

Transparency log

Statistics

Installs: 1

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

v0.1.0 2026-07-21 12:50 UTC

This package is not auto-updated.

Last update: 2026-07-22 01:41:05 UTC


README

View discovery and native PHP template rendering for the IviPHP ecosystem.

Overview

iviphp/view provides a small, framework-independent view layer for server-rendered PHP applications.

It includes:

  • named view discovery;
  • dot-notation view names;
  • multiple template directories;
  • native PHP template rendering;
  • isolated template execution;
  • output buffering;
  • shared template data;
  • configurable template extensions;
  • cached view-path resolution;
  • strict view-name validation;
  • strict template-variable validation;
  • safe rendering exceptions;
  • no global static view state.

The package focuses on server-rendered PHP views. SPA asset management, frontend bundling, hydration and client-side routing belong in separate integration packages.

Installation

composer require iviphp/view

Requirements

  • PHP 8.2 or later
  • Composer
  • iviphp/support

Package structure

src/
├── Engines/
│   └── PhpEngine.php
├── Exceptions/
│   └── ViewException.php
├── View.php
├── ViewFinder.php
├── ViewInterface.php
└── ViewManager.php

Basic usage

Create a view directory:

resources/
└── views/
    └── home.php

Create the template:

<!-- resources/views/home.php -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></title>
</head>
<body>
    <h1><?= htmlspecialchars($heading, ENT_QUOTES, 'UTF-8') ?></h1>
</body>
</html>

Configure the view service:

<?php

declare(strict_types=1);

use Ivi\View\View;

$view = View::fromPaths([
    __DIR__ . '/resources/views',
]);

Render the template:

$html = $view->render(
    'home',
    [
        'title' => 'IviPHP',
        'heading' => 'Welcome to IviPHP',
    ]
);

The rendered output is returned as a string.

The package does not send output directly to the client.

Configuration

Create a view instance from configuration:

$view = View::fromConfig([
    'paths' => [
        __DIR__ . '/resources/views',
    ],

    'extension' => 'php',

    'shared' => [
        'applicationName' => 'IviPHP',
    ],
]);

Supported configuration fields:

Field Required Default Description
paths yes none Template directories
extension no php Template file extension
shared no [] Variables shared with every view

At least one readable view directory is required.

View names

Views are referenced using dot notation.

$view->render('home');
$view->render('users.profile');
$view->render('admin.dashboard.index');

These names resolve to:

home.php
users/profile.php
admin/dashboard/index.php

A view name may contain:

  • letters;
  • numbers;
  • underscores;
  • hyphens;
  • dots separating path segments.

Valid view names:

home
users.profile
admin.dashboard
components.alert-box
errors.404

Invalid view names:

../home
users/profile
users\profile
.users
users.
users..profile

Directory traversal and direct filesystem paths are rejected.

Checking whether a view exists

if ($view->exists('users.profile')) {
    $html = $view->render(
        'users.profile',
        [
            'user' => $user,
        ]
    );
}

exists() returns false when the named view cannot be found.

Invalid view names still produce a ViewException.

Rendering views

$html = $view->render(
    'users.profile',
    [
        'user' => [
            'id' => 42,
            'name' => 'Gaspard',
        ],
    ]
);

Inside resources/views/users/profile.php:

<article>
    <h1>
        <?= htmlspecialchars(
            $user['name'],
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </h1>

    <p>User ID: <?= (int) $user['id'] ?></p>
</article>

The make() alias

make() is an alias of render():

$html = $view->make(
    'users.profile',
    [
        'user' => $user,
    ]
);

Template variables

Associative array keys become local variables inside the PHP template.

$html = $view->render(
    'article',
    [
        'title' => 'Building with IviPHP',
        'content' => 'A small framework can still be powerful.',
        'published' => true,
    ]
);

Inside the template:

<article>
    <h1>
        <?= htmlspecialchars(
            $title,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </h1>

    <p>
        <?= htmlspecialchars(
            $content,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </p>

    <?php if ($published): ?>
        <span>Published</span>
    <?php endif; ?>
</article>

Template variable names must be valid PHP variable identifiers.

Valid names:

user
currentUser
application_name
pageTitle

Invalid names:

user-name
page.title
42user

Reserved template variables

The rendering engine rejects variables that could interfere with PHP internals or the rendering scope.

Reserved names include:

GLOBALS
_SERVER
_GET
_POST
_FILES
_COOKIE
_SESSION
_REQUEST
_ENV
this

Variables beginning with __ivi are also reserved.

$view->render(
    'home',
    [
        '__iviPath' => 'invalid',
    ]
);

This produces a ViewException.

Escaping output

The view engine does not automatically escape template values.

Escape untrusted HTML output explicitly:

<?= htmlspecialchars(
    $userInput,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?>

Integer values can be cast:

<?= (int) $userId ?>

JSON data can be encoded safely:

<script>
    window.applicationData = <?= json_encode(
        $applicationData,
        JSON_THROW_ON_ERROR
        | JSON_HEX_TAG
        | JSON_HEX_AMP
        | JSON_HEX_APOS
        | JSON_HEX_QUOT
    ) ?>;
</script>

Shared view data

Share a variable with every rendered view:

$view->share(
    'applicationName',
    'IviPHP'
);

The variable becomes available in every template:

<title>
    <?= htmlspecialchars(
        $applicationName,
        ENT_QUOTES,
        'UTF-8'
    ) ?>
</title>

Share multiple values:

$view->shareMany([
    'applicationName' => 'IviPHP',
    'environment' => 'production',
    'currentYear' => 2026,
]);

Overriding shared data

Data passed directly to render() overrides shared data with the same name.

$view->share(
    'title',
    'Default title'
);

$html = $view->render(
    'home',
    [
        'title' => 'Home page',
    ]
);

The template receives:

Home page

Inspecting shared data

Check whether a shared variable exists:

$view->hasShared('applicationName');

Retrieve a shared variable:

$name = $view->shared(
    'applicationName',
    'Application'
);

Return every shared variable:

$data = $view->allShared();

Removing shared data

Remove one shared variable:

$view->forgetShared('environment');

Remove all shared variables:

$view->flushShared();

Multiple view directories

$view = View::fromPaths([
    __DIR__ . '/resources/views',
    __DIR__ . '/modules/blog/views',
]);

Directories are searched in declaration order.

When the same named view exists in multiple directories, the first matching file is used.

Example:

resources/views/layouts/app.php
modules/blog/views/layouts/app.php

With this configuration:

$view = View::fromPaths([
    __DIR__ . '/resources/views',
    __DIR__ . '/modules/blog/views',
]);

layouts.app resolves to:

resources/views/layouts/app.php

Adding view directories

$view->addPath(
    __DIR__ . '/modules/shop/views'
);

The new path is appended to the search list.

Prepend a path so it has higher priority:

$view->prependPath(
    __DIR__ . '/themes/custom/views'
);

Equivalent:

$view->addPath(
    __DIR__ . '/themes/custom/views',
    true
);

Removing view directories

$view->removePath(
    __DIR__ . '/modules/shop/views'
);

Removing a missing or inaccessible path has no effect.

Replacing all view directories

$view->setPaths([
    __DIR__ . '/new/views',
    __DIR__ . '/shared/views',
]);

All cached view-path resolutions are cleared automatically.

Inspecting view directories

$paths = $view->paths();

The returned paths are canonical absolute filesystem paths.

Resolving a view path

$path = $view->path('users.profile');

Example result:

/var/www/application/resources/views/users/profile.php

A ViewException is thrown when the view cannot be found.

View path caching

Resolved view paths are cached in memory.

$view->render('home');
$view->render('home');

The second render can reuse the previously resolved path.

The template file is still executed again. Only filesystem discovery is cached.

Forgetting one resolved path

$view->forget('home');

The next render searches the configured directories again.

Clearing all resolved paths

$view->flush();

This clears cached view-path resolutions.

It does not remove shared data or template files.

Custom template extension

The default extension is .php.

A different extension can be configured:

$view = View::fromPaths(
    paths: [
        __DIR__ . '/resources/views',
    ],
    extension: 'phtml'
);

A view named:

users.profile

then resolves to:

users/profile.phtml

However, the included PhpEngine only renders files ending in .php.

For the initial package, use the default php extension with PhpEngine.

Additional template engines can be introduced in later versions.

View manager

Create a ViewFinder:

use Ivi\View\ViewFinder;

$finder = new ViewFinder([
    __DIR__ . '/resources/views',
]);

Create a manager:

use Ivi\View\ViewManager;

$manager = new ViewManager($finder);

Render a view:

$html = $manager->render(
    'home',
    [
        'title' => 'IviPHP',
    ]
);

Create a manager directly from paths:

$manager = ViewManager::fromPaths(
    paths: [
        __DIR__ . '/resources/views',
    ],
    sharedData: [
        'applicationName' => 'IviPHP',
    ]
);

Creating the main view entry point

use Ivi\View\View;

$view = new View($manager);

The View class delegates its operations to ViewManager.

View finder

Use ViewFinder directly:

use Ivi\View\ViewFinder;

$finder = new ViewFinder([
    __DIR__ . '/resources/views',
]);

$path = $finder->find('users.profile');

Check existence:

$exists = $finder->exists('users.profile');

Return the generated relative path:

$relativePath = $finder->relativePath(
    'users.profile'
);

Result:

users/profile.php

Finder extension

Return the configured extension:

$extension = $finder->extension();

Change it:

$finder->setExtension('php');

Changing the extension clears all cached path resolutions.

PHP rendering engine

Use the engine directly:

use Ivi\View\Engines\PhpEngine;

$engine = new PhpEngine();

$html = $engine->render(
    view: 'home',
    path: __DIR__ . '/resources/views/home.php',
    data: [
        'title' => 'IviPHP',
    ]
);

The engine:

  1. validates the view name;
  2. validates the template path;
  3. validates template variable names;
  4. opens an output buffer;
  5. executes the template inside an isolated closure;
  6. captures the generated output;
  7. restores the output-buffer state;
  8. returns the rendered output.

Output buffering

Template output is captured automatically:

<h1>Hello</h1>

Rendering returns:

<h1>Hello</h1>

The engine does not call echo outside the template and does not write directly to the HTTP response.

Output-buffer protection

Templates must not leave the output-buffer stack in an inconsistent state.

Avoid manually opening or closing output buffers inside templates:

<?php ob_start(); ?>

The engine detects unexpected output-buffer changes and produces a ViewException.

Template includes

A template can include another trusted PHP file:

<?php include __DIR__ . '/../partials/header.php'; ?>

<main>
    <h1>
        <?= htmlspecialchars(
            $title,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </h1>
</main>

<?php include __DIR__ . '/../partials/footer.php'; ?>

Direct includes are normal PHP behavior.

The included file is not resolved through ViewFinder.

For reusable application views, rendering named templates through View or ViewManager is preferred.

Simple layout example

Directory structure:

resources/views/
├── layouts/
│   └── app.php
└── pages/
    └── home.php

resources/views/pages/home.php:

<section>
    <h1>
        <?= htmlspecialchars(
            $heading,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </h1>

    <p>
        <?= htmlspecialchars(
            $message,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </p>
</section>

Render the page content:

$content = $view->render(
    'pages.home',
    [
        'heading' => 'Welcome',
        'message' => 'Your application is ready.',
    ]
);

Render the layout:

$html = $view->render(
    'layouts.app',
    [
        'title' => 'Home',
        'content' => $content,
    ]
);

resources/views/layouts/app.php:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">

    <title>
        <?= htmlspecialchars(
            $title,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </title>
</head>
<body>
    <?= $content ?>
</body>
</html>

Only render trusted HTML content without escaping.

Component example

Create:

resources/views/components/alert.php

Template:

<div class="alert alert-<?= htmlspecialchars(
    $type,
    ENT_QUOTES,
    'UTF-8'
) ?>">
    <?= htmlspecialchars(
        $message,
        ENT_QUOTES,
        'UTF-8'
    ) ?>
</div>

Render:

$alert = $view->render(
    'components.alert',
    [
        'type' => 'success',
        'message' => 'The operation completed successfully.',
    ]
);

Rendering collections

$html = $view->render(
    'users.index',
    [
        'users' => [
            [
                'id' => 1,
                'name' => 'Alice',
            ],
            [
                'id' => 2,
                'name' => 'Bob',
            ],
        ],
    ]
);

Template:

<ul>
    <?php foreach ($users as $user): ?>
        <li>
            <?= htmlspecialchars(
                $user['name'],
                ENT_QUOTES,
                'UTF-8'
            ) ?>
        </li>
    <?php endforeach; ?>
</ul>

Conditional rendering

<?php if ($user !== null): ?>
    <p>
        Welcome,
        <?= htmlspecialchars(
            $user['name'],
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </p>
<?php else: ?>
    <a href="/login">Sign in</a>
<?php endif; ?>

Templates are regular PHP files and support normal PHP syntax.

View exceptions

View failures throw:

Ivi\View\Exceptions\ViewException

Example:

use Ivi\View\Exceptions\ViewException;

try {
    $html = $view->render(
        'users.profile',
        [
            'user' => $user,
        ]
    );
} catch (ViewException $exception) {
    error_log(
        json_encode([
            'message' => $exception->getMessage(),
            'context' => $exception->context(),
        ])
    );
}

Safe exception context

ViewException provides diagnostic context without including template values.

$context = $exception->context();

Example:

[
    'view' => 'users.profile',
    'path' => '/var/www/application/resources/views/users/profile.php',
]

Invalid data failures only include variable names:

[
    'view' => 'users.profile',
    'variables' => [
        'user',
        'pageTitle',
    ],
]

Template values are never added to exception context.

Exception factories

use Ivi\View\Exceptions\ViewException;

ViewException::invalidViewName(
    $view,
    $reason
);

ViewException::viewNotFound(
    $view,
    $paths
);

ViewException::invalidPath(
    $path,
    $reason
);

ViewException::directoryUnavailable(
    $directory
);

ViewException::viewUnreadable(
    $view,
    $path
);

ViewException::renderFailed(
    $view,
    $path,
    $previous
);

ViewException::invalidData(
    $view,
    $variables,
    $reason
);

ViewException::unsupportedEngine(
    $engine
);

ViewException::invalidConfiguration(
    $engine,
    $reason
);

Error handling

use Ivi\View\Exceptions\ViewException;

try {
    $html = $view->render(
        'dashboard.index',
        [
            'user' => $currentUser,
            'statistics' => $statistics,
        ]
    );
} catch (ViewException $exception) {
    error_log(
        json_encode([
            'message' => $exception->getMessage(),
            'context' => $exception->context(),
        ])
    );

    $html = '<h1>Unable to render this page.</h1>';
}

Do not expose filesystem paths or internal rendering errors directly to application users in production.

Complete example

Project structure:

application/
├── public/
│   └── index.php
└── resources/
    └── views/
        ├── layouts/
        │   └── app.php
        └── pages/
            └── home.php

public/index.php:

<?php

declare(strict_types=1);

use Ivi\View\Exceptions\ViewException;
use Ivi\View\View;

require dirname(__DIR__) . '/vendor/autoload.php';

$view = View::fromConfig([
    'paths' => [
        dirname(__DIR__) . '/resources/views',
    ],

    'shared' => [
        'applicationName' => 'IviPHP',
        'currentYear' => 2026,
    ],
]);

try {
    $content = $view->render(
        'pages.home',
        [
            'heading' => 'Modern PHP, kept simple.',
            'message' => 'Build your application from focused packages.',
        ]
    );

    $html = $view->render(
        'layouts.app',
        [
            'title' => 'Home',
            'content' => $content,
        ]
    );

    echo $html;
} catch (ViewException $exception) {
    error_log(
        json_encode([
            'message' => $exception->getMessage(),
            'context' => $exception->context(),
        ])
    );

    http_response_code(500);

    echo 'Unable to render the page.';
}

resources/views/pages/home.php:

<section>
    <h1>
        <?= htmlspecialchars(
            $heading,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </h1>

    <p>
        <?= htmlspecialchars(
            $message,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </p>
</section>

resources/views/layouts/app.php:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">

    <meta
        name="viewport"
        content="width=device-width, initial-scale=1"
    >

    <title>
        <?= htmlspecialchars(
            $title,
            ENT_QUOTES,
            'UTF-8'
        ) ?>

        ·

        <?= htmlspecialchars(
            $applicationName,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </title>
</head>
<body>
    <header>
        <strong>
            <?= htmlspecialchars(
                $applicationName,
                ENT_QUOTES,
                'UTF-8'
            ) ?>
        </strong>
    </header>

    <main>
        <?= $content ?>
    </main>

    <footer>
        &copy; <?= (int) $currentYear ?>
    </footer>
</body>
</html>

Available classes

ViewInterface

$view->exists($name);

$view->render(
    $name,
    $data
);

View

View::fromPaths(
    $paths,
    $sharedData,
    $extension
);

View::fromConfig(
    $configuration
);

$view->manager();
$view->finder();

$view->exists($name);
$view->path($name);

$view->render(
    $name,
    $data
);

$view->make(
    $name,
    $data
);

$view->share(
    $name,
    $value
);

$view->shareMany($data);

$view->hasShared($name);

$view->shared(
    $name,
    $default
);

$view->allShared();

$view->forgetShared($name);
$view->flushShared();

$view->addPath(
    $path,
    $prepend
);

$view->prependPath($path);
$view->removePath($path);
$view->setPaths($paths);
$view->paths();

$view->forget($name);
$view->flush();

ViewManager

ViewManager::fromPaths(
    $paths,
    $sharedData,
    $extension
);

$manager->finder();
$manager->engine();

$manager->exists($name);
$manager->path($name);

$manager->render(
    $name,
    $data
);

$manager->make(
    $name,
    $data
);

$manager->share(
    $name,
    $value
);

$manager->shareMany($data);

$manager->hasShared($name);

$manager->shared(
    $name,
    $default
);

$manager->allShared();

$manager->forgetShared($name);
$manager->flushShared();

$manager->addPath(
    $path,
    $prepend
);

$manager->prependPath($path);
$manager->removePath($path);
$manager->setPaths($paths);
$manager->paths();

$manager->forget($name);
$manager->flush();

ViewFinder

$finder->addPath(
    $path,
    $prepend
);

$finder->prependPath($path);
$finder->removePath($path);
$finder->setPaths($paths);

$finder->paths();

$finder->extension();
$finder->setExtension($extension);

$finder->exists($view);
$finder->find($view);
$finder->relativePath($view);

$finder->forget($view);
$finder->flush();

PhpEngine

$engine->render(
    $view,
    $path,
    $data
);

ViewException

$exception->getMessage();
$exception->context();
$exception->getPrevious();

Security considerations

Escape untrusted output

The package does not automatically escape values.

Always escape untrusted content before inserting it into HTML:

<?= htmlspecialchars(
    $value,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?>

Do not expose arbitrary template selection

Do not render a view name directly from user input:

$view->render($_GET['view']);

Even though names are validated, template selection should remain controlled by the application.

Use an allowlist:

$pages = [
    'home' => 'pages.home',
    'about' => 'pages.about',
];

$page = $_GET['page'] ?? 'home';
$template = $pages[$page] ?? 'errors.404';

$html = $view->render($template);

Keep template directories trusted

Templates are executable PHP files.

Only trusted application code should be stored in view directories.

Do not allow users to upload PHP files into these directories.

Protect filesystem paths

Do not return view exception paths directly to users in production.

Filesystem paths may reveal internal application structure.

Escape attributes separately

Use HTML escaping inside attributes:

<a href="<?= htmlspecialchars(
    $url,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?>">
    Open
</a>

Trusted rendered fragments

A rendered child view may be inserted into a layout without escaping:

<?= $content ?>

Only do this when $content was generated by trusted application templates.

SPA integration

This package focuses on server-rendered PHP templates.

A future SPA integration package can provide:

  • Vite manifest discovery;
  • JavaScript and CSS asset helpers;
  • development-server detection;
  • frontend entry-point resolution;
  • preload tags;
  • hydration data;
  • frontend route fallback;
  • Vue, React or Svelte integration;
  • server-side rendering bridges.

Those features should remain separate from the core view package so applications can use native PHP views without installing frontend tooling.

A PHP view can already host a SPA entry point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta
        name="viewport"
        content="width=device-width, initial-scale=1"
    >

    <title>
        <?= htmlspecialchars(
            $title,
            ENT_QUOTES,
            'UTF-8'
        ) ?>
    </title>
</head>
<body>
    <div id="app"></div>

    <script
        type="module"
        src="/assets/application.js"
    ></script>
</body>
</html>

Asset discovery and build-tool integration can be added later through a dedicated package.

Design principles

  • Small framework-independent API
  • Native PHP templates
  • Dot-notation view names
  • Multiple ordered view directories
  • Strict path validation
  • Strict variable-name validation
  • Isolated template execution
  • Captured template output
  • Shared application data
  • Cached view discovery
  • No hidden global view state
  • No automatic output escaping
  • No automatic HTTP response emission
  • Safe rendering diagnostics
  • Clear separation from frontend tooling

Package boundaries

This package is responsible for:

  • named view discovery;
  • template directory management;
  • native PHP rendering;
  • output capture;
  • shared template data;
  • view-path caching;
  • rendering exceptions.

It is not responsible for:

  • HTTP responses;
  • routing;
  • controllers;
  • frontend bundling;
  • SPA routing;
  • Vite integration;
  • JavaScript compilation;
  • CSS compilation;
  • server-side JavaScript rendering;
  • automatic HTML escaping;
  • authentication;
  • authorization;
  • application bootstrapping.

These responsibilities belong to other IviPHP packages or application code.

Current limitations

The initial release does not include:

  • template inheritance syntax;
  • template directives;
  • automatic layouts;
  • components as PHP objects;
  • view composers;
  • namespaces;
  • package view registration;
  • compiled templates;
  • automatic HTML escaping;
  • frontend asset manifests;
  • Vite integration;
  • SPA hydration;
  • React, Vue or Svelte adapters;
  • server-side JavaScript rendering.

The package intentionally starts with a small native PHP rendering foundation.

Ecosystem

This package is part of the IviPHP ecosystem:

  • iviphp/contracts
  • iviphp/support
  • iviphp/config
  • iviphp/container
  • iviphp/http
  • iviphp/validation
  • iviphp/database
  • iviphp/cache
  • iviphp/view
  • iviphp/auth
  • iviphp/framework

Contributing

Contributions should preserve the focused and secure design of the package.

Changes should:

  • preserve strict view-name validation;
  • prevent directory traversal;
  • preserve isolated PHP rendering;
  • preserve output-buffer restoration;
  • avoid exposing template data in exceptions;
  • avoid automatic HTTP output;
  • remain independent from the full framework;
  • avoid hidden global state;
  • keep frontend tooling outside the core package;
  • support PHP 8.2 and later.

Security

Please report security vulnerabilities privately to the Softadastra maintainers instead of opening a public issue.

License

This project is licensed under the MIT License.

Maintainer

Created and maintained by Softadastra.