no-framework-tutorial/implementation/15-adding-content/data/pages/11-templating.md

241 lines
9.1 KiB
Markdown
Raw Normal View History

[<< previous](10-invoker.md) | [next >>](12-configuration.md)
### Templating
A template engine is not necessary with PHP because the language itself can take care of that. But it can make things
like escaping values easier. They also make it easier to draw a clear line between your application logic and the
template files which should only put your variables into the HTML code.
A good quick read on this is [ircmaxell on templating](http://blog.ircmaxell.com/2012/12/on-templating.html). Please
also read [this](http://chadminick.com/articles/simple-php-template-engine.html) for a different opinion on the topic.
Personally I don't have a strong opinion on the topic, so decide yourself which approach works better for you.
For this tutorial we will use a PHP implementation of [Mustache](https://github.com/bobthecow/mustache.php). So install
that package before you continue (`composer require mustache/mustache`).
Another well known alternative would be [Twig](http://twig.sensiolabs.org/).
Now please go and have a look at the source code of the
[engine class](https://github.com/bobthecow/mustache.php/blob/master/src/Mustache/Engine.php). As you can see, the class
does not implement an interface.
You could just type hint against the concrete class. But the problem with this approach is that you create tight
coupling.
In other words, all your code that uses the engine will be coupled to this mustache package. If you want to change the
implementation you have a problem. Maybe you want to switch to Twig, maybe you want to write your own class or you want
to add functionality to the engine. You can't do that without going back and changing all your code that is tightly
coupled.
What we want is loose coupling. We will type hint against an interface and not a class/implementation. So if you need
another implementation, you just implement that interface in your new class and inject the new class instead.
Instead of editing the code of the package we will use the [adapter pattern](http://en.wikipedia.org/wiki/Adapter_pattern).
This sounds a lot more complicated than it is, so just follow along.
First let's define the interface that we want. Remember the [interface segregation principle](http://en.wikipedia.org/wiki/Interface_segregation_principle).
This means that instead of large interfaces with a lot of methods we want to make each interface as small as possible.
A class can implement multiple interfaces if necessary.
So what does our template engine actually need to do? For now we really just need a simple `render` method. Create a
new folder in your `src/` folder with the name `Template` where you can put all the template related things.
In there create a new interface `Renderer.php` that looks like this:
```php
<?php declare(strict_types = 1);
namespace Lubian\NoFramework\Template;
interface Renderer
{
/**
* @param string $template
* @param array<string, mixed> $data
* @return string
*/
public function render(string $template, array $data = []) : string;
}
```
Now that this is sorted out, let's create the implementation for mustache. In the same folder, create the file
`MustacheRenderer.php` with the following content:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
final class MustacheRenderer implements Renderer
{
public function __construct(private \Mustache_Engine $engine){}
public function render(string $template, array $data = []): string
{
return $this->engine->render($template, $data);
}
}
```
As you can see the adapter is really simple. While the original class had a lot of methods, our adapter is really simple
and only fulfills the interface.
Of course we also have to add a definition in our `dependencies.php` file because otherwise the container won't know
which implementation he has to inject when you hint for the interface. Add this line:
```php
[
...
\Lubian\NoFramework\Template\Renderer::class => DI\create(\Lubian\NoFramework\Template\MustacheRenderer::class)
->constructor(new Mustache_Engine),
]
```
Now update the Hello.php class to require an implementation of our renderer interface
and use that to render a string using mustache syntax.
```php
final class Hello
{
public function __invoke(
ResponseInterface $response,
Now $now,
Renderer $renderer,
string $name = 'Stranger',
): ResponseInterface {
$body = $response->getBody();
$data = [
'now' => $now()->format('H:i:s'),
'name' => $name,
];
$content = $renderer->render(
'Hello {{name}}, the time is {{now}}!',
$data,
);
$body->write($content);
return $response
->withStatus(200)
->withBody($body);
}
}
```
Now go check quickly in your browser if everything works. By default Mustache uses a simple string handler.
But what we want is template files, so let's go back and change that.
To make this change we need to pass an options array to the `Mustache_Engine` constructor. So let's go back to the
`dependencies.php` file and add the following code:
```php
[
...
Mustache_Loader_FilesystemLoader::class => fn() => new Mustache_Loader_FilesystemLoader(__DIR__ . '/../templates', ['extension' => '.html']),
Mustache_Engine::class => fn (Mustache_Loader_FilesystemLoader $MLFsl) => new Mustache_Engine(['loader' => $MLFsl]),
]
```
We are passing an options array because we want to use the `.html` extension instead of the default `.mustache` extension.
Why? Other template languages use a similar syntax and if we ever decide to change to something else then we won't have
to rename all the template files.
To let PHP-DI use its magic for creating our MustacheRenderer class we need to tell it exactly how to wire all the
dependencies, therefore I defined how to create the Filesystemloader, on the next line we typehinted that loader
in the short closure which acts as a factory method for the Mustache_Engine, as PHP-DI automatically injects the Object
we can then use it in the factory.
In your project root folder, create a `templates` folder. In there, create a file `hello.html`. The content of the file should look like this:
```
<h1>Hello World</h1>
Hello {{ name }}
```
Now you can go back to your `Hello` action and change the render line to `$html = $this->renderer->render('hello', $data);`
Navigate to the hello page in your browser to make sure everything works.
One thing that still bothers me is the fact that we have some configuration paths scattered in our dependencies
file. We could add a simple valueobject to our code that gives us a typesafe access to our configuration
values.
Lets create a 'Settings' class in our './src' Folder:
```php
<?php
declare(strict_types=1);
namespace Lubian\NoFramework;
final class Settings
{
public function __construct(
public readonly string $environment,
public readonly string $templateDir,
public readonly string $templateExtension,
){}
}
```
I am using a new Feature from PHP 8.1 here called [readonly properties](https://stitcher.io/blog/php-81-readonly-properties) to write a small valueobject without the need to write complex getters and setters. The linked article gives a great explanation on how they work.
When creating an instance of the setting class with my project specific values i will use another
new feature called [named arguments](https://stitcher.io/blog/php-8-named-arguments). There is
a lot of discussion on the topic of named arguments as some argue it creates unclean and
unmaintainable code, but vor simple valueobjects i would argue that they are ok.
here is a small example of creating a settings object using named arguments.
```php
$setting = new Settings(
environment: getenv('ENVIRONMENT') ?: 'dev',
templateDir: getenv('TEMPLATE_DIR') ?: __DIR__ . '/../templates',
templateExtension: getenv('TEMPLATE_EXTENSION') ?: '.html',
);
```
lets put that code in a file called settings.php in our config folder, and return the settings object
from there
Here is my updated dependencies.php with some imports and aliases added for my convenience:
```php
<?php
declare(strict_types=1);
use DI\ContainerBuilder;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Lubian\NoFramework\Settings;
use Lubian\NoFramework\Template\Mustache;
use Lubian\NoFramework\Template\Renderer;
use Mustache_Engine as ME;
use Mustache_Loader_FilesystemLoader as MLFsl;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use function DI\create;
$builder = new ContainerBuilder();
$builder->addDefinitions([
Settings::class => fn () => require __DIR__ '/settings.php',
ResponseInterface::class => create(Response::class),
ServerRequestInterface::class => fn () => ServerRequestFactory::fromGlobals(),
Renderer::class => fn (ME $me) => new Mustache($me),
MLFsl::class => fn (Settings $s) => new MLFsl($s->templateDir, ['extension' => $s->templateExtension]),
ME::class => fn (MLFsl $MLFsl) => new ME(['loader' => $MLFsl]),
]);
return $builder->build();
```
And as always, don't forget to commit your changes.
[<< previous](10-invoker.md) | [next >>](12-configuration.md)