Skip to content

Commit 5000bf3

Browse files
authored
Merge pull request #6 from maplephp/feat/twig
Twig template
2 parents 629b8d7 + 6581717 commit 5000bf3

9 files changed

Lines changed: 385 additions & 25 deletions

File tree

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11

22
APP_TITLE="My App"
3+
4+
# development, test, staging, production
35
APP_ENV="development"
46

57
# Database driver: mysql | sqlite

README.md

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ The goal is not to lock you into a fixed ecosystem. Each `maplephp/*` library is
2222
- [Middleware](#middleware)
2323
- [Database](#database)
2424
- [Validation](#validation)
25+
- [Aborting Requests](#aborting-requests)
2526
- [Error Handling](#error-handling)
27+
- [Twig Templates](#twig-templates)
2628
- [Caching](#caching)
2729
- [Logging](#logging)
2830
- [CLI Commands](#cli-commands)
@@ -577,13 +579,13 @@ use MaplePHP\Validate\Validator;
577579

578580
// Option 1: Create an instance
579581
$v = new Validator($email);
580-
if ($v->isEmail() && $v->length(5, 255)) {
581-
// value is valid
582+
if (!($v->isEmail() && $v->length(5, 255))) {
583+
abort(422, 'Invalid email address');
582584
}
583585

584586
// Option 2: Use the static method for cleaner syntax
585-
if (Validator::value($email)->isEmail(1, 200)) {
586-
// value is valid
587+
if (!Validator::value($email)->isEmail(1, 200)) {
588+
abort(422, 'Invalid email address');
587589
}
588590
```
589591

@@ -593,6 +595,222 @@ Visit the [maplephp/validate](https://github.com/maplephp/validate) repository f
593595

594596
---
595597

598+
## Aborting Requests
599+
600+
The global `abort()` function is the standard way to stop a request and return an HTTP error response. It throws an `HttpException` that the `HttpStatusError` middleware catches and renders as an error page.
601+
602+
```php
603+
// Trigger a 404
604+
abort(404);
605+
606+
// With a custom message
607+
abort(404, 'User not found');
608+
609+
// With extra props forwarded to the error page renderer
610+
abort(403, 'Access denied', ['required_role' => 'admin']);
611+
```
612+
613+
`abort()` can be called from anywhere — controllers, services, commands. No `return` is needed; execution stops at the throw.
614+
615+
### Aborting After Validation
616+
617+
A common pattern is to abort immediately when input validation fails:
618+
619+
```php
620+
use MaplePHP\Validate\ValidationChain;
621+
622+
public function store(ResponseInterface $response, ServerRequestInterface $request): ResponseInterface
623+
{
624+
$body = $request->getParsedBody();
625+
626+
$email = new ValidationChain($body['email'] ?? '');
627+
$email->isEmail()->length(5, 255);
628+
629+
if ($email->hasError()) {
630+
abort(422, 'Invalid email address');
631+
}
632+
633+
// continue processing ...
634+
return $response;
635+
}
636+
```
637+
638+
### Passing Props to the Error Page
639+
640+
The third `$props` argument is forwarded as part of `$context` in `ErrorPageInterface::render()`, making it available to custom error page templates:
641+
642+
```php
643+
abort(403, 'Access denied', ['redirect' => '/login']);
644+
```
645+
646+
In the error page renderer: `$context['redirect']` will be `'/login'`.
647+
648+
---
649+
650+
## Custom HTTP Error Pages
651+
652+
By default, `HttpStatusError` renders a built-in styled page for any `abort()` call or non-2xx response. The page shows the status code, a message, and a "Go home" link.
653+
654+
To replace it, implement `MaplePHP\Core\Interfaces\ErrorPageInterface`:
655+
656+
```php
657+
// app/Errors/MyErrorPage.php
658+
namespace App\Errors;
659+
660+
use MaplePHP\Core\Interfaces\ErrorPageInterface;
661+
use Psr\Http\Message\ResponseInterface;
662+
use Psr\Http\Message\ServerRequestInterface;
663+
664+
class MyErrorPage implements ErrorPageInterface
665+
{
666+
public function render(
667+
ResponseInterface $response,
668+
ServerRequestInterface $request,
669+
array $context = []
670+
): string {
671+
$code = $response->getStatusCode();
672+
$message = $context['message'] ?? $response->getReasonPhrase();
673+
674+
// Return an HTML string — require a view file, use a template engine, etc.
675+
ob_start();
676+
require App::get()->dir()->resources() . '/errors/error.php';
677+
return ob_get_clean();
678+
}
679+
}
680+
```
681+
682+
Available in `render()`:
683+
684+
| Variable | Source | Description |
685+
|---|---|---|
686+
| `$response->getStatusCode()` | PSR-7 | HTTP status code (404, 403, 500, …) |
687+
| `$response->getReasonPhrase()` | PSR-7 | Default HTTP reason phrase |
688+
| `$context['message']` | `abort()` arg 2 | Custom message passed to `abort()` |
689+
| `$context[*]` | `abort()` arg 3 | Any props from the third `abort()` argument |
690+
| `$request->getUri()` | PSR-7 | The current request URI |
691+
692+
Register the custom renderer in `configs/http.php` by passing it directly to `HttpStatusError`:
693+
694+
```php
695+
// configs/http.php
696+
use App\Errors\MyErrorPage;
697+
use MaplePHP\Core\Middlewares\HttpStatusError;
698+
use MaplePHP\Emitron\Middlewares\ContentLengthMiddleware;
699+
700+
return [
701+
"middleware" => [
702+
"global" => [
703+
new HttpStatusError(new MyErrorPage()),
704+
ContentLengthMiddleware::class,
705+
]
706+
]
707+
];
708+
```
709+
710+
---
711+
712+
## Twig Templates
713+
714+
MaplePHP ships with built-in Twig support via `TwigServiceProvider` and the `MaplePHP\Core\Support\Twig` helper. Enable it by adding the provider to `configs/providers.php`:
715+
716+
```php
717+
// configs/providers.php
718+
return [
719+
\MaplePHP\Core\Providers\DatabaseProvider::class,
720+
\MaplePHP\Core\Providers\TwigServiceProvider::class, // Remove this line if you don't use Twig
721+
];
722+
```
723+
724+
The provider registers a `Twig\Environment` in the container, using `resources/` as the template root. It enables file caching in production and debug mode in development automatically.
725+
726+
### Template Structure
727+
728+
```
729+
resources/
730+
├── index.twig # Base layout — extend this in every view
731+
├── views/ # View templates
732+
│ └── hello.twig
733+
└── errors/ # Error page templates (used by TwigErrorPage)
734+
└── error.twig
735+
```
736+
737+
### Layout and Inheritance
738+
739+
`resources/index.twig` is the base layout. Define your shared `<html>`, `<head>`, and chrome there, and expose named blocks for child templates to fill in:
740+
741+
A child view extends it and fills the blocks:
742+
743+
### Rendering in a Controller
744+
745+
Inject `MaplePHP\Core\Support\Twig` as a parameter — the framework resolves it automatically. Call `render()` with a path relative to `resources/` and an array of template variables:
746+
747+
```php
748+
// app/Controllers/HelloController.php
749+
namespace App\Controllers;
750+
751+
use MaplePHP\Core\Routing\DefaultController;
752+
use MaplePHP\Core\Support\Twig;
753+
use MaplePHP\Http\Interfaces\PathInterface;
754+
use Psr\Http\Message\ResponseInterface;
755+
756+
class HelloController extends DefaultController
757+
{
758+
public function show(Twig $twig, PathInterface $path): void
759+
{
760+
$twig->render('views/hello.twig', [
761+
'title' => 'Hello',
762+
'name' => $path->select("name")->last() ?: 'World',
763+
])
764+
}
765+
}
766+
```
767+
768+
`render()` writes the rendered HTML to the response body, and returns the `ResponseInterface` instance for further processing if you wish.
769+
770+
To add globals, extensions, or filters, access the underlying environment via `$twig->getEnvironment()`:
771+
772+
```php
773+
$twig->getEnvironment()->addGlobal('app_name', 'My App');
774+
```
775+
776+
### Twig Error Pages
777+
778+
`TwigErrorPage` renders `resources/errors/error.twig` and passes the following variables:
779+
780+
| Variable | Description |
781+
|---|---|
782+
| `{{ code }}` | HTTP status code (404, 500, …) |
783+
| `{{ message }}` | Reason phrase or custom `abort()` message |
784+
| `{{ uri }}` | The request URI that triggered the error |
785+
| `{{ context }}` | Full context array from `abort()` |
786+
787+
---
788+
789+
## Error page
790+
791+
To render HTTP error responses with;
792+
* Twig, register `TwigErrorPage` in `configs/http.php`:
793+
* Vanilla, register `HttpStatusError::class` in `configs/http.php`:
794+
795+
```php
796+
// configs/http.php
797+
use MaplePHP\Core\Middlewares\HttpStatusError;
798+
use MaplePHP\Core\Render\Errors\TwigErrorPage;
799+
use MaplePHP\Emitron\Middlewares\ContentLengthMiddleware;
800+
801+
return [
802+
"middleware" => [
803+
"global" => [
804+
// HttpStatusError::class, // Will load PHP vanilla HTTP Status Error page
805+
// new HttpStatusError(new TwigErrorPage()), // Will load Twig HTTP Status Error page
806+
ContentLengthMiddleware::class,
807+
]
808+
]
809+
];
810+
```
811+
812+
---
813+
596814
## Caching
597815

598816
`maplephp/cache` implements both PSR-6 (`CacheItemPoolInterface`) and PSR-16 (`CacheInterface`). The `Cache` wrapper provides the simple PSR-16 API around any PSR-6 handler.

app/Controllers/HelloWorldController.php

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
namespace App\Controllers;
66

7+
use MaplePHP\Core\Render\StaticRenderer;
8+
use MaplePHP\Core\Support\Twig;
9+
use MaplePHP\DTO\Format\Clock;
710
use Psr\Http\Message\ResponseInterface;
811
use MaplePHP\Core\Routing\DefaultController;
912
use MaplePHP\Http\Interfaces\PathInterface;
1013

11-
1214
class HelloWorldController extends DefaultController
1315
{
1416
/**
@@ -18,30 +20,33 @@ class HelloWorldController extends DefaultController
1820
* modified response is returned to the framework.
1921
*
2022
* @param ResponseInterface $response Response instance used to write output
21-
* @param PathInterface $path Provides access to route parameters and segments
23+
* @param StaticRenderer $render
2224
* @return ResponseInterface
2325
*/
24-
public function index(ResponseInterface $response, PathInterface $path): ResponseInterface
25-
{
26-
$response->getBody()->write("Hello World!<br>");
27-
$response->getBody()->write("<a href='" . $path->uri()->withPath("/show") . "'>Show</a>");
28-
return $response;
29-
}
26+
public function index(ResponseInterface $response, StaticRenderer $render): ResponseInterface
27+
{
28+
$html = $render->welcome();
29+
$response->getBody()->write($html);
30+
return $response;
31+
}
3032

3133
/**
32-
* Example route demonstrating how to read values from the path.
34+
* Twig-rendered example page.
35+
* Renders resources/views/hello.twig with a set of props passed as template variables.
3336
*
34-
* The response body is written to using the PSR-7 stream and the
35-
* modified response is returned to the framework.
36-
*
37-
* @param ResponseInterface $response Response instance used to write output
38-
* @param PathInterface $path Provides access to route parameters and segments
39-
* @return ResponseInterface
37+
* @param Twig $twig
38+
* @param PathInterface $path
39+
* @throws \Exception
4040
*/
41-
public function show(ResponseInterface $response, PathInterface $path): ResponseInterface
41+
public function show(Twig $twig, PathInterface $path): void
4242
{
43-
$response->getBody()->write("Hello World 2!<br>");
44-
$response->getBody()->write("Hello form: " . $path->select("page")->last());
45-
return $response;
43+
$name = $path->select("name")->last() ?: 'World';
44+
45+
$twig->render('views/hello.twig', [
46+
'title' => 'Hello from Twig',
47+
'name' => $name,
48+
'rendered_at' => Clock::value('now')->dateTime(),
49+
'items' => ['Twig templates', 'PSR-7 responses', 'Dependency injection'],
50+
]);
4651
}
4752
}

configs/http.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
use MaplePHP\Core\Middlewares\HttpStatusError;
44
use MaplePHP\Emitron\Middlewares\ContentLengthMiddleware;
5+
use MaplePHP\Core\Render\Errors\TwigErrorPage;
56
use MaplePHP\Emitron\Middlewares\GzipMiddleware;
67

78
return [
89
"middleware" => [
910
"global" => [
10-
HttpStatusError::class,
11+
new HttpStatusError(new TwigErrorPage()),
1112
ContentLengthMiddleware::class,
1213
//GzipMiddleware::class
1314
]

configs/providers.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
return [
44
\MaplePHP\Core\Providers\DatabaseProvider::class,
5+
\MaplePHP\Core\Providers\TwigServiceProvider::class,
56
];

0 commit comments

Comments
 (0)