├── phpstan.neon
├── phpcs.xml
├── phpunit.xml
├── LICENSE.md
├── src
├── TwigExtension.php
├── TwigRuntimeLoader.php
├── TwigMiddleware.php
├── TwigRuntimeExtension.php
└── Twig.php
└── composer.json
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths:
4 | - src
5 | ignoreErrors:
6 | - message: '#Method (.*) has parameter (.*) with generic class Slim\\App but does not specify its types: TContainerInterface#'
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Slim coding standard
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | src
16 | tests
17 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | tests
12 |
13 |
14 |
15 |
16 |
17 | src
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Josh Lockhart
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/src/TwigExtension.php:
--------------------------------------------------------------------------------
1 | routeParser = $routeParser;
35 | $this->uri = $uri;
36 | $this->basePath = $basePath;
37 | }
38 |
39 | /**
40 | * Create the runtime implementation of a Twig element.
41 | *
42 | * @param string $class
43 | *
44 | * @return mixed
45 | */
46 | public function load(string $class)
47 | {
48 | if (TwigRuntimeExtension::class === $class) {
49 | return new $class($this->routeParser, $this->uri, $this->basePath);
50 | }
51 |
52 | return null;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slim/twig-view",
3 | "description": "Slim Framework 4 view helper built on top of the Twig 3 templating component",
4 | "license": "MIT",
5 | "type": "library",
6 | "keywords": [
7 | "slim",
8 | "framework",
9 | "view",
10 | "template",
11 | "twig"
12 | ],
13 | "authors": [
14 | {
15 | "name": "Josh Lockhart",
16 | "email": "hello@joshlockhart.com",
17 | "homepage": "http://joshlockhart.com"
18 | },
19 | {
20 | "name": "Pierre Berube",
21 | "email": "pierre@lgse.com",
22 | "homepage": "http://www.lgse.com"
23 | }
24 | ],
25 | "homepage": "https://www.slimframework.com",
26 | "require": {
27 | "php": "^7.4 || ^8.0",
28 | "psr/http-message": "^1.1 || ^2.0",
29 | "slim/slim": "^4.12",
30 | "symfony/polyfill-php81": "^1.29",
31 | "twig/twig": "^3.11"
32 | },
33 | "require-dev": {
34 | "phpstan/phpstan": "^1.10.59",
35 | "phpunit/phpunit": "^9.6 || ^10",
36 | "psr/http-factory": "^1.0",
37 | "squizlabs/php_codesniffer": "^3.9"
38 | },
39 | "autoload": {
40 | "psr-4": {
41 | "Slim\\Views\\": "src"
42 | }
43 | },
44 | "autoload-dev": {
45 | "psr-4": {
46 | "Slim\\Tests\\": "tests"
47 | }
48 | },
49 | "scripts": {
50 | "sniffer:check": "phpcs --standard=phpcs.xml",
51 | "sniffer:fix": "phpcbf --standard=phpcs.xml",
52 | "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi",
53 | "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always",
54 | "test:all": [
55 | "@sniffer:check",
56 | "@stan",
57 | "@test:coverage"
58 | ],
59 | "test:coverage": [
60 | "@putenv XDEBUG_MODE=coverage",
61 | "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --coverage-clover build/coverage/clover.xml --coverage-html build/coverage --coverage-text"
62 | ]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/TwigMiddleware.php:
--------------------------------------------------------------------------------
1 | getContainer();
42 | if ($container === null) {
43 | throw new RuntimeException('The app does not have a container.');
44 | }
45 | if (!$container->has($containerKey)) {
46 | throw new RuntimeException(
47 | "The specified container key does not exist: $containerKey"
48 | );
49 | }
50 |
51 | $twig = $container->get($containerKey);
52 | if (!($twig instanceof Twig)) {
53 | throw new RuntimeException(
54 | "Twig instance could not be resolved via container key: $containerKey"
55 | );
56 | }
57 |
58 | return new self(
59 | $twig,
60 | $app->getRouteCollector()->getRouteParser(),
61 | $app->getBasePath()
62 | );
63 | }
64 |
65 | /**
66 | * @param App $app
67 | * @param Twig $twig
68 | * @param string $attributeName
69 | *
70 | * @return TwigMiddleware
71 | */
72 | public static function create(App $app, Twig $twig, string $attributeName = 'view'): self
73 | {
74 | return new self(
75 | $twig,
76 | $app->getRouteCollector()->getRouteParser(),
77 | $app->getBasePath(),
78 | $attributeName
79 | );
80 | }
81 |
82 | /**
83 | * @param Twig $twig
84 | * @param RouteParserInterface $routeParser
85 | * @param string $basePath
86 | * @param string|null $attributeName
87 | */
88 | public function __construct(
89 | Twig $twig,
90 | RouteParserInterface $routeParser,
91 | string $basePath = '',
92 | ?string $attributeName = null
93 | ) {
94 | $this->twig = $twig;
95 | $this->routeParser = $routeParser;
96 | $this->basePath = $basePath;
97 | $this->attributeName = $attributeName;
98 | }
99 |
100 | /**
101 | * Process an incoming server request.
102 | *
103 | * @param ServerRequestInterface $request
104 | * @param RequestHandlerInterface $handler
105 | *
106 | * @return ResponseInterface
107 | */
108 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
109 | {
110 | $runtimeLoader = new TwigRuntimeLoader($this->routeParser, $request->getUri(), $this->basePath);
111 | $this->twig->addRuntimeLoader($runtimeLoader);
112 |
113 | if ($this->attributeName !== null) {
114 | $request = $request->withAttribute($this->attributeName, $this->twig);
115 | }
116 |
117 | return $handler->handle($request);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/TwigRuntimeExtension.php:
--------------------------------------------------------------------------------
1 | routeParser = $routeParser;
32 | $this->uri = $uri;
33 | $this->basePath = $basePath;
34 | }
35 |
36 | /**
37 | * Get the url for a named route
38 | *
39 | * @param string $routeName Route name
40 | * @param array $data Route placeholders
41 | * @param array $queryParams Query parameters
42 | *
43 | * @return string
44 | */
45 | public function urlFor(string $routeName, array $data = [], array $queryParams = []): string
46 | {
47 | return $this->routeParser->urlFor($routeName, $data, $queryParams);
48 | }
49 |
50 | /**
51 | * Get the full url for a named route
52 | *
53 | * @param string $routeName Route name
54 | * @param array $data Route placeholders
55 | * @param array $queryParams Query parameters
56 | *
57 | * @return string
58 | */
59 | public function fullUrlFor(string $routeName, array $data = [], array $queryParams = []): string
60 | {
61 | return $this->routeParser->fullUrlFor($this->uri, $routeName, $data, $queryParams);
62 | }
63 |
64 | /**
65 | * @param string $routeName Route name
66 | * @param array $data Route placeholders
67 | *
68 | * @return bool
69 | */
70 | public function isCurrentUrl(string $routeName, array $data = []): bool
71 | {
72 | $currentUrl = $this->basePath . $this->uri->getPath();
73 | $result = $this->routeParser->urlFor($routeName, $data);
74 |
75 | return $result === $currentUrl;
76 | }
77 |
78 | /**
79 | * Get current path on given Uri
80 | *
81 | * @param bool $withQueryString
82 | *
83 | * @return string
84 | */
85 | public function getCurrentUrl(bool $withQueryString = false): string
86 | {
87 | $currentUrl = $this->basePath . $this->uri->getPath();
88 | $query = $this->uri->getQuery();
89 |
90 | if ($withQueryString && !empty($query)) {
91 | $currentUrl .= '?' . $query;
92 | }
93 |
94 | return $currentUrl;
95 | }
96 |
97 | /**
98 | * Get the uri
99 | *
100 | * @return UriInterface
101 | */
102 | public function getUri(): UriInterface
103 | {
104 | return $this->uri;
105 | }
106 |
107 | /**
108 | * Set the uri
109 | *
110 | * @param UriInterface $uri
111 | *
112 | * @return self
113 | */
114 | public function setUri(UriInterface $uri): self
115 | {
116 | $this->uri = $uri;
117 |
118 | return $this;
119 | }
120 |
121 | /**
122 | * Get the base path
123 | *
124 | * @return string
125 | */
126 | public function getBasePath(): string
127 | {
128 | return $this->basePath;
129 | }
130 |
131 | /**
132 | * Set the base path
133 | *
134 | * @param string $basePath
135 | *
136 | * @return self
137 | */
138 | public function setBasePath(string $basePath): self
139 | {
140 | $this->basePath = $basePath;
141 |
142 | return $this;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Twig.php:
--------------------------------------------------------------------------------
1 |
44 | */
45 | class Twig implements ArrayAccess
46 | {
47 | /**
48 | * Twig loader
49 | */
50 | protected LoaderInterface $loader;
51 |
52 | /**
53 | * Twig environment
54 | */
55 | protected Environment $environment;
56 |
57 | /**
58 | * Default view variables
59 | *
60 | * @var array
61 | */
62 | protected array $defaultVariables = [];
63 |
64 | /**
65 | * @param ServerRequestInterface $request
66 | * @param string $attributeName
67 | *
68 | * @return Twig
69 | */
70 | public static function fromRequest(ServerRequestInterface $request, string $attributeName = 'view'): self
71 | {
72 | $twig = $request->getAttribute($attributeName);
73 | if (!($twig instanceof self)) {
74 | throw new RuntimeException(
75 | 'Twig could not be found in the server request attributes using the key "' . $attributeName . '".'
76 | );
77 | }
78 |
79 | return $twig;
80 | }
81 |
82 | /**
83 | * @param string|string[] $path Path(s) to templates directory
84 | * @param array $settings Twig environment settings
85 | *
86 | * @throws LoaderError When the template cannot be found
87 | *
88 | * @return Twig
89 | */
90 | public static function create($path, array $settings = []): self
91 | {
92 | $loader = new FilesystemLoader();
93 |
94 | $paths = is_array($path) ? $path : [$path];
95 | foreach ($paths as $namespace => $path) {
96 | if (is_string($namespace)) {
97 | $loader->setPaths($path, $namespace);
98 | } else {
99 | $loader->addPath($path);
100 | }
101 | }
102 |
103 | return new self($loader, $settings);
104 | }
105 |
106 | /**
107 | * @param LoaderInterface $loader Twig loader
108 | * @param array $settings Twig environment settings
109 | */
110 | public function __construct(LoaderInterface $loader, array $settings = [])
111 | {
112 | $this->loader = $loader;
113 | $this->environment = new Environment($this->loader, $settings);
114 | $extension = new TwigExtension();
115 | $this->addExtension($extension);
116 | }
117 |
118 | /**
119 | * Proxy method to add an extension to the Twig environment
120 | *
121 | * @param ExtensionInterface $extension A single extension instance or an array of instances
122 | */
123 | public function addExtension(ExtensionInterface $extension): void
124 | {
125 | $this->environment->addExtension($extension);
126 | }
127 |
128 | /**
129 | * Proxy method to add a runtime loader to the Twig environment
130 | *
131 | * @param RuntimeLoaderInterface $runtimeLoader
132 | */
133 | public function addRuntimeLoader(RuntimeLoaderInterface $runtimeLoader): void
134 | {
135 | $this->environment->addRuntimeLoader($runtimeLoader);
136 | }
137 |
138 | /**
139 | * Fetch rendered template
140 | *
141 | * @param string $template Template pathname relative to templates directory
142 | * @param array $data Associative array of template variables
143 | *
144 | * @throws LoaderError When the template cannot be found
145 | * @throws SyntaxError When an error occurred during compilation
146 | * @throws RuntimeError When an error occurred during rendering
147 | *
148 | * @return string
149 | */
150 | public function fetch(string $template, array $data = []): string
151 | {
152 | $data = array_merge($this->defaultVariables, $data);
153 |
154 | return $this->environment->render($template, $data);
155 | }
156 |
157 | /**
158 | * Fetch rendered block
159 | *
160 | * @param string $template Template pathname relative to templates directory
161 | * @param string $block Name of the block within the template
162 | * @param array $data Associative array of template variables
163 | *
164 | * @throws Throwable When an error occurred during rendering
165 | * @throws LoaderError When the template cannot be found
166 | * @throws SyntaxError When an error occurred during compilation
167 | *
168 | * @return string
169 | */
170 | public function fetchBlock(string $template, string $block, array $data = []): string
171 | {
172 | $data = array_merge($this->defaultVariables, $data);
173 |
174 | return $this->environment->resolveTemplate($template)->renderBlock($block, $data);
175 | }
176 |
177 | /**
178 | * Fetch rendered string
179 | *
180 | * @param string $string String
181 | * @param array $data Associative array of template variables
182 | *
183 | * @throws LoaderError When the template cannot be found
184 | * @throws SyntaxError When an error occurred during compilation
185 | *
186 | * @return string
187 | */
188 | public function fetchFromString(string $string = '', array $data = []): string
189 | {
190 | $data = array_merge($this->defaultVariables, $data);
191 |
192 | return $this->environment->createTemplate($string)->render($data);
193 | }
194 |
195 | /**
196 | * Output rendered template
197 | *
198 | * @param ResponseInterface $response
199 | * @param string $template Template pathname relative to templates directory
200 | * @param array $data Associative array of template variables
201 | *
202 | * @throws LoaderError When the template cannot be found
203 | * @throws SyntaxError When an error occurred during compilation
204 | * @throws RuntimeError When an error occurred during rendering
205 | *
206 | * @return ResponseInterface
207 | */
208 | public function render(ResponseInterface $response, string $template, array $data = []): ResponseInterface
209 | {
210 | $response->getBody()->write($this->fetch($template, $data));
211 |
212 | return $response;
213 | }
214 |
215 | /**
216 | * Return Twig loader
217 | *
218 | * @return LoaderInterface
219 | */
220 | public function getLoader(): LoaderInterface
221 | {
222 | return $this->loader;
223 | }
224 |
225 | /**
226 | * Return Twig environment
227 | *
228 | * @return Environment
229 | */
230 | public function getEnvironment(): Environment
231 | {
232 | return $this->environment;
233 | }
234 |
235 | /**
236 | * Does this collection have a given key?
237 | *
238 | * @param string $key The data key
239 | *
240 | * @return bool
241 | */
242 | public function offsetExists($key): bool
243 | {
244 | return array_key_exists($key, $this->defaultVariables);
245 | }
246 |
247 | /**
248 | * Get collection item for key
249 | *
250 | * @param string $key The data key
251 | *
252 | * @return mixed The key's value, or the default value
253 | */
254 | #[ReturnTypeWillChange]
255 | public function offsetGet($key)
256 | {
257 | if (!$this->offsetExists($key)) {
258 | return null;
259 | }
260 | return $this->defaultVariables[$key];
261 | }
262 |
263 | /**
264 | * Set collection item
265 | *
266 | * @param string $key The data key
267 | * @param mixed $value The data value
268 | */
269 | public function offsetSet($key, $value): void
270 | {
271 | $this->defaultVariables[$key] = $value;
272 | }
273 |
274 | /**
275 | * Remove item from collection
276 | *
277 | * @param string $key The data key
278 | */
279 | public function offsetUnset($key): void
280 | {
281 | unset($this->defaultVariables[$key]);
282 | }
283 |
284 | /**
285 | * Get number of items in collection
286 | *
287 | * @return int
288 | */
289 | public function count(): int
290 | {
291 | return count($this->defaultVariables);
292 | }
293 |
294 | /**
295 | * Get collection iterator
296 | *
297 | * @return ArrayIterator
298 | */
299 | public function getIterator(): ArrayIterator
300 | {
301 | return new ArrayIterator($this->defaultVariables);
302 | }
303 | }
304 |
--------------------------------------------------------------------------------