riverside / php-express

:horse: PHP micro-framework inspired by Express.js
https://riverside.github.io/php-express/
MIT License
26 stars 10 forks source link

Support other template engines #9

Open jameswilson opened 4 months ago

jameswilson commented 4 months ago

Thanks for your work on this project. I'm planning on using it to put together a simple HTMX example app.

It would be interesting to add in optional support for the Twig template engine. Regardless of anyone's stance on Symfony, it is a fantastic template language and doesn't even require the entire Symfony package ecosystem to use it standalone.

Something like this would be cool:

composer require twig/twig
$app = new \PhpExpress\Application();
$app->set('views', 'path/to/views');
$app->set('twig_options', [
  'cache' => 'path/to/twig_cache',

  // Uncomment for local development only.
  // 'debug' => true,
  // 'auto_reload' => true,
]);

$app->get('/', function ($req, $res) {
    $res->render('base', [
        'title' => 'Home page',
        'message' => 'This is the home page',
    ]);
});

$app->get('about', function ($req, $res) {
    $res->render('about', [
        'title' => 'About page',
        'message' => 'This is the about page',
    ]);
});

Then in template folder we have:

views/base.twig:

<html>
<head>
<title>{{ title }}</title>
</head>
<body>
{% block header %}
<h1>{{ title }}</h1>
{% endblock %}
{% block content %}
<div>{{ content }}</div>
<a href="/about">About</a>
{% endblock %}
</body>
</html>

views/about.html.twig:

{% extends "base.twig" %}
{% block content %}
<div>{{ content }}</div>
<a href="/">Home</a>
{% endblock %}

In Application::render() we can hand down local variables into the Twig template if a matching view filename with extension .twig is found.

Then we can check if Twig FilesystemLoader class exists. The following is working for me:

    public function render(string $view, array $locals=null): void
    {
        if ($locals)
        {
            extract($locals);
        }
        $views_dir = $this->set("views");
        $layout = sprintf("%s/%s.php", $views_dir, $view);
        $_twig = sprintf("%s/%s.twig", $views_dir, $view);
        $_template = sprintf("%s/%s.php", $views_dir, $this->set("template"));
        if (is_file($_template))
        {
            include $_template;
        } elseif (is_file($_twig)) {
            if (!class_exists('\Twig\Loader\FilesystemLoader')) {
                $filepath = realpath($_twig);
                throw new \Exception("Please `composer require twig/twig` to proceed. A Twig template '{$filepath}' was found but the Twig template engine could not be loaded");
            }
            $loader = new \Twig\Loader\FilesystemLoader($views_dir);
            $twig = new \Twig\Environment($loader, $this->set("twig_options"));
            echo $twig->render(sprintf('%s.twig', $view), $locals);
        } else {
            include $layout;
        }
    }
jameswilson commented 4 months ago

Thinking through this a bit more, maybe the template engine approach should be generalized to support any kind of PHP template engine such as Laravel Blade (here is a Blade template engine standalone package) or the Crow template engine .

For example have a special app setting register a mapping mechanism from file extension to render callback function. That way you don't have to build in 3rd party tools into your codebase.

Something like:

$app = new \PhpExpress\Application();
$app->set('views', 'path/to/views');
$app->set('template_engines', [
  'twig' => function(string $view, array $locals = null): void {
    $loader = new \Twig\Loader\FilesystemLoader('path/to/views');
    $twig = new \Twig\Environment($loader, [
      'cache' => 'path/to/twig_cache',

      // Uncomment for local development only.
      'debug' => true,
      'auto_reload' => true,
    ]);
    echo $twig->render($template, $locals);
  }
]);

Or maybe a way to completely override the render function via callback without needing to subclass \PhpExpress\Application ?

$app->set('renderer', function(string $view, array $locals = null): void {
    $loader = new \Twig\Loader\FilesystemLoader('path/to/views');
    $twig = new \Twig\Environment($loader, [
      'cache' => 'path/to/twig_cache',

      // Uncomment for local development only.
      'debug' => true,
      'auto_reload' => true,
    ]);
    echo $twig->render($view, $locals);
});

$app->get('/', function ($req, $res) {
  $res->render('base.html.twig', [
    'title' => 'Home page',
    'content' => 'This is the home page.',
  ]);
});