A simple yet powerful PHP template engine that doesn't create a new 'language' for you to learn and get used to. Uses vanilla PHP for everything, doesn't require any regex or evals, and simple caching capabilities. Could be extended to whatever you might need it to. Good for those small projects where using a framework is just too much.

class ViewEngine
{

    // Enabling auto-deletion of expired cache files ()
    protected $autoDeleteCache = true;

    // Length of life of cache files (In minutes)
    protected $cacheExpiration = 1;  // 1440 24 hours

    // Toggle caching on/off
    protected $enableCache = false;

    // In case we don't want cache to ever expire
    protected $permanentCache = false;

    // Various variables in-use with the template engine. Not settings
    protected $includedViews = [];
    protected $sections = [];
    protected $layout;
    protected $templateDir;
    protected $cacheDir;
    protected $layoutData;

    public function __construct()
    {
        // Location of view/template files
        $this->templateDir = baseDir('resources/views');

        // Location of view/template cache
        $this->cacheDir = baseDir('storage/cache/views');
    }

    public function enableCache($enable = true)
    {
        $this->enableCache = $enable;
    }

    public function extend($layout, $data = [])
    {
        $this->layout = $layout;
        $this->layoutData = $data;
        $this->render($layout, $data);
    }

    public function section($name, $default = '')
    {
        return $this->sections[$name] ?? $default;
    }

    public function startSection()
    {
        ob_start();
    }

    public function endSection($name)
    {
        $this->sections[$name] = ob_get_clean();
    }

    public function include($view, $data = [])
    {
        $includedViewPath = $this->templateDir . '/' . $view . '.php';
        $this->includedViews[$view] = $includedViewPath;

        ob_start();
        extract($data);
        include $includedViewPath;
        return ob_get_clean();
    }

    public function render($view, $data = [])
    {
        if ($this->enableCache) {
            $cacheFile = $this->getCacheFileName($view);

            if (!$this->isCacheExpired($cacheFile)) {
                // Cache is still valid, include cached file
                return include $cacheFile;;
            }
        }

        extract($data);

        ob_start();

        $templatePath = $this->templateDir . '/' . $view . '.php';
        include $templatePath;

        if ($this->layout) {
            // Extract layout data and merge with view data
            $layoutData = array_merge($this->layoutData, $data);
            extract($layoutData);

            include "{$this->templateDir}/{$this->layout}.php";
        }

        $content = ob_get_clean();

        if ($this->enableCache) {
            // Save content to cache file
            file_put_contents($cacheFile, $content);
        }

        return $content;
    }

    public function setCacheExpiration($minutes)
    {
        $this->cacheExpiration = $minutes;
    }

    protected function isCacheExpired($cacheFile)
    {
        if ($this->enableCache && file_exists($cacheFile)) {
            if ($this->permanentCache === true) {
                return false;
            }
            $fileTime = filemtime($cacheFile);
            $expirationTime = $fileTime + ($this->cacheExpiration * 60);
            return time() > $expirationTime;
        }

        $this->deleteExpiredCache($cacheFile);
        return true;
    }

    protected function getCacheFileName($view)
    {
        return $this->cacheDir . '/' . md5($view) . '.php';
    }

    public function enableAutoDeleteCache($enable = true)
    {
        $this->autoDeleteCache = $enable;
    }

    public function enablePermanentCache($enable = true)
    {
        $this->permanentCache = $enable;
    }

    protected function deleteExpiredCache($cacheFile)
    {
        if ($this->enableCache && $this->autoDeleteCache) {
            if (file_exists($cacheFile) && $this->isCacheExpired($cacheFile)) {
                unlink($cacheFile);
            }
        }
    }
}

This code snippet was published on It was last edited on

Contributing Authors

1

1 Comment

  • Votes
  • Oldest
  • Latest
BO
450 9
Commented
Updated
// layout.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $title ?></title>
</head>
<body>
    <?= $this->section('content') ?>
    <!-- some more content -->
    <?= $this->section('section2') ?>
</body>
</html>

// index.php

<?php $this->extend('layout', ['title' => 'page title']);

$this->startSection(); ?>
Some content that will go into the section named 'content'. The title passed to the layout above could've being passed from the controller. Not required to pass it from the view that is extending the layout.

<?= $this->include('include', ['feature' => 'As Requested']) ?>

We included a view into this view that will go into the section 'content' on our layout page.
<?php $this->endSection('content');

$this->startSection(); ?>
Some more content that will go into the section named 'section2'. Doesn't matter how much HTML there is between the two sections.
<?php $this->endSection('section2'); ?>

// controller.php

return view('view', $data);

// The view helper function

function view($template, $data)
{
    return (new ViewEngine)->render($template, $data);
}

Simple usage example. Returns the content to be passed to your response class, or you could just echo if you aren't using a response class.

  • 0
    <?= $this->include('path/to/file', ['feature' => 'As Requested']) ?> The file you include could also be in a different directory. Starting from the base template directory. — Bogey
add a comment
0