dakujem / latter
Latte view layer for PSR-7 frameworks and stacks.
Requires
- php: >=7.4
- psr/http-message: ^1.0
Requires (Dev)
- dakujem/sleeve: ^1.1
- latte/latte: ^2.5
- nette/tester: ^2.3.1
- nyholm/psr7: ^1.2
- slim/slim: ^4
This package is auto-updated.
Last update: 2024-10-29 10:54:15 UTC
README
Latte view layer for PSR-7 frameworks and stacks. It's not a typo.
๐ฟ
composer require dakujem/latter
To use the awesome Latte templating language with a PSR-7 compliant framework like Slim, one can either struggle to set everything up by himself or use Latter.
The latter is a better choice
- Latter is designed to reduce code repetition (render pipelines, render routines)
- Latter is a flexible thin layer that can be tuned and tamed as one desires
- set up once and forget
๐
Check out the Latte documentation to get fluent in Latte.
Render Latte template to PSR-7 response
A very basic example to render a Latte template like the following
{*} hello.latte {*} Hello {$name}!
in Slim framework using Latter would be:
// app.php $app = AppFactory::create(); // ... $app->get('/hello/{name}', function (Request $request, Response $response, array $args) { $params = [ 'name' => $args['name'], ]; return (new Dakujem\Latter\View)->render( $response, __DIR__ . '/templates/hello.latte', $params, new Latte\Engine ); }); // ... $app->run();
It would of course be an overkill to use Latte in a case like the one above.
Most of the time, one will use Latte for more complicated templates with multiple variables, filters and macros.
In such applications, it will make sense to define a Latter\View
service in a service container for dependency injection.
Cofigure Latte\Engine factory service
First, let's create a service container.
I'll be using Sleeve, a trivial extension of Symfony Pimple container.
$container = new Dakujem\Sleeve();
In most cases, a new Latte\Engine
instance should be created for each template render. That is why a factory service should be defined. That is, every time the service is requested from the service container, a new instance will be returned.
๐ก
Check out the documentation for the service container or framework you are using to configure this step correctly.
$container->set('latte', $container->factory(function () use ($container) { $engine = new Latte\Engine(); // Configure the file loader to search for templates in a dedicated directory. $loader = new Latte\Loaders\FileLoader(__DIR__ . '/templates'); $engine->setLoader($loader); // Set a temporary directory, where compiled Latte templates will be stored. $engine->setTempDirectory($container->settings['view-temp-dir']); return $engine; }));
The definition should contain:
- installation of common filters
- installation of custom tags (macros)
- configuration of an appropriate Latte loader
- any other
Latte\Engine
setup calls needed
Now every time we call $container->get('latte')
, a new instance of a configured Latte\Engine
will be returned:
(new Dakujem\Latter\View)->render( $response, 'hello.latte', $params, $container->get('latte') );
Note that we no longer need to prefix the template name with a full path, because of the FileLoader
configuration.
Configure Latter\View service
Now let's define a Latter\View
service.
$container->set('view', function () use ($container) { $view = new Dakujem\Latter\View(); // optionally set an engine factory (recommended) $view->setEngine(function () use ($container): Latte\Engine { return $container->get('latte'); }); return $view; });
An instance of Latter\View
will now be available in the service container:
$view = $container->get('view');
If an engine factory is provided to the View
service, it is possible to omit providing the Engine
instance for each rendered template.
// the render calls have gotten shorter: $view->render($response, 'hello.latte', $params);
The View
service definition can contain these optional definitions:
- template aliases
- render routines (template rendering)
- render pipelines
- engine factory
- default parameters
- default render routine
Each of these are optional.
Template aliases
It is possible to create template aliases, so that the templates can be referred to using a different name.
$view->alias('hello', 'hello.latte'); $view->alias('index', 'ClientModule/Index/default.latte');
To render a template using its alias:
$view->render($response, 'hello', $params); $view->render($response, 'index', $params);
Render routines
Render routines should be used to apply template-specific setup without the need for code repetition.
They may be used to
- define filters
- define tags (macros)
- modify input parameters
- modify template name
- or even to use a completely different Engine instance or render own Response
A render routine is a callable that receives a Runtime
context object and returns a response, with the following signature:
function(Dakujem\Latter\Runtime $context): Psr\Http\Message\ResponseInterface | Dakujem\Latter\Runtime
Example:
$view->register('shopping-cart', function (Runtime $context) { // This callable is the place to register filters, // variables and stuff for template named "shopping-cart" // Do any setup of the Engine that is needed for the template to render correctly $latte = $context->getEngine(); $latte->addFilter('count', function(){ // return the count of items in the shopping cart here }); // Template name can be set or changed freely. // Note that if one only needs to set a nice name for the template to be rendered, // aliases are a simpler option to do so $template = 'ClientModule/Cart/list.latte'; // The params can be modified at will, for example to provide defaults $params = array_merge(['default' => 'value'], $context->getParams()); // the Runtime::toResponse helper method can be used for default rendering return $context->withTarget($template)->withParams($params); });
One can render the routine exactly as he would render an alias:
$view->render($response, 'shopping-cart', $params);
Default render routine
A default render routine may optionally be registered, that will be used for all non-registered templates.
$view->registerDefault( function (Runtime $context) { ... } );
The default render routine has exactly the same signature as the named ones.
It will be used when rendering a template that has not been registered.
$view->render($response, 'a-template-with-no-registered-routine', $params);
Default parameters
Default parameters are merged with the parameters provided to each render call.
If one wants to define per-template default parameters, render routines can be used.
$view->setParam('userName', 'Guest'); // a single parameter $view->setParams([ 'userName' => 'Guest', 'projectName' => 'My Awesome Project', ]); // all parameters at once
Render pipelines
Pipelines allow multiple pre-render routines to be called one after another before the final rendering.
The routines can be shared across multiple template render calls that share a common layout, common include(s), common setup (filters, variables) or other rendering logic.
The most obvious case being layouts or common file / block includes.
First, appropriate pre-render routines have to be registered:
$view->register('base-layout', function (Runtime $context) { // do setup needed for templates using the base layout $context->getEngine()->addFilter( ... ); // return a context object (!) return $context; }); $view->register('--withUser--', function (Runtime $context) { // do setup common for templates using a `$user` variable $defaults = [ 'user' => get_user( 'somehow' ), ]; // return a context object (!) return $context->withParams($defaults); });
For pre-render routines used in pipelines, it is important to return a Runtime
context object. If a Response
was returned, the pipeline would end prematurely (this might be desired in certain cases though). Return value of any other kind is ignored.
Render calls using pipelines could look like these:
// calling a pipeline with 2 _pre-render_ routines and a registered render routine $view ->pipeline('base-layout', '--withUser--') ->render($response, 'shopping-cart', $params); // rendering a file with a common _pre-render_ routine $view ->pipeline('--withUser--') ->render($response, 'userProfile.latte', $params);
Pipelines are particularly useful when dealing with included templates (header, footer) or layout templates that require specific variables or filters to render.
Example:
{*} home.latte {*} {layout 'base.latte'} {block #content} <p>Greetings, stranger!</p>
{*} about.latte {*} {layout 'base.latte'} {block #content} <p>Stay awhile and listen.</p>
Both the above share the same layout, that needs specific setup done in the 'base-layout'
pre-render routine:
$view->pipeline('base-layout')->render($response, 'home.latte'); $view->pipeline('base-layout')->render($response, 'about.latte');
This kind of rendering could be compared to tagging or decorating templates before rendering.
Alternatively, it is also possible to define the pipeline as a part of the rendering routine:
$view->register('contacts.latte', $view->pipeline('base-layout', function (Runtime $context) { // ... do whatever setup needed for rendering the contacts page return $context->withParams(['foo' => 'bar']); }));
{*} contacts.latte {*} {layout 'base.latte'} {block #content} <p>Contact: {$foo}</p>
$view->render($response, 'contacts.latte');
Explicit chaining
Sometimes it is desired to invoke one rendering routine from within another. This is possible using View::another
or View::execute
.
// register a routine named 'ahoy', that will render `hello.latte` $view->register('ahoy', function (Runtime $context) { return $context->withTarget('hello.latte'); }); // register a routine that will internally invoke it $view->register('foo', function (Runtime $context) use ($view) { return $view->another($context, $view->getRoutine('ahoy')); }); // render 'hello.latte' using 'foo' routine that internally uses 'ahoy' routine $view->render($response, 'foo');
Note that these methods are not limited to using registered routines, they can execute any callable provided its signature fits.
Tips & tricks
Performance
Latte templates are compiled on first render. All subsequent renders will use compiled and optimized PHP code.
To slightly improve performance on production servers, auto-refresh can be turned off in the Engine factory:
$engine->setAutoRefresh($container->settings['dev'] ?? true);
This has its caveats, read the Latte docs beforehand.
Use {link} and n:href macros with Slim framework
It is possible to define {link}
and n:href
macros that behave similarly to the macros in Nette framework.
These macros will generate URLs for named routes.
First make sure a filter that generates the URL is registered in the Latte Engine, then create a macro that uses the filter.
$app = Slim\Factory\AppFactory::create(); $engine = new Latte\Engine(); // The section below configures the `{link}` and `n:href` macros and the `urlFor` filter. $engine->addFilter('urlFor', function ($route, ...$args) use ($app) { // the filter will call the `urlFor` method of the route parser return $app->getRouteCollector()->getRouteParser()->urlFor($route, ...$args); // Note: if you are using slim v3, use `$container->get('router')->pathFor( ... )` instead. }); $macroSet = new MacroSet($engine->getCompiler()); $linkMacro = function (MacroNode $node, PhpWriter $writer) { return $writer->using($node)->write('echo ($this->filters->urlFor)(%node.word, %node.args?);'); }; $macroSet->addMacro('link', $linkMacro); $macroSet->addMacro('href', null, null, function (MacroNode $node, PhpWriter $writer) use ($linkMacro) { return ' ?> href="<?php ' . $linkMacro($node, $writer) . ' ?>"<?php '; });
The above would be best done during the
'latte'
service definition. See the test for more details.
Then it is possible to use the macros:
{*} named routes without route parameters {*} {link home} {*} named routes wit route parameters {*} {link hello [name => $name]} {*} named routes with route parameters and query parameters {*} {link rc [resource => apple, action => eat], [param1 => val1, param2 => val2]} {*} n:href macro has the same syntax {*} <a n:href='home'>go home</a> <a n:href='hello [name => hugo], [a => b]'>polite hello</a> {*} using the filter is of course possible too {*} {='home'|urlFor}
๐ก
Note the difference to Nette framework - the first macro argument must be an array.
In order for the query parameters to work,%node.args?
is used in the macro. It is possible to replace%node.args?
with%node.array?
in order to ditch query parameters in favor of exact Nette {link} syntax.
Setting up the Engine before rendering
It is the intention of render routines to provide a place to set up the Latte\Engine
instance before rendering a particular template, however, pipelines can be used to quickly set the engine up:
$view->pipeline(function(Runtime $context) { $context->getEngine()->addFilter( ... ); })->render($response, 'myTemplate.latte', ['param' => 'value']);
Contributions
... are welcome. ๐ต
Go ahead and fork the repository, make your changes, then submit your PR.