├── LICENSE.md ├── composer.json ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml └── src ├── Twig.php ├── TwigExtension.php ├── TwigMiddleware.php ├── TwigRuntimeExtension.php └── TwigRuntimeLoader.php /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 | -------------------------------------------------------------------------------- /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 | "phpspec/prophecy-phpunit": "^2.0", 35 | "phpstan/phpstan": "^1.10.59", 36 | "phpunit/phpunit": "^9.6 || ^10", 37 | "psr/http-factory": "^1.0", 38 | "squizlabs/php_codesniffer": "^3.9" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Slim\\Views\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Slim\\Tests\\": "tests" 48 | } 49 | }, 50 | "scripts": { 51 | "sniffer:check": "phpcs --standard=phpcs.xml", 52 | "sniffer:fix": "phpcbf --standard=phpcs.xml", 53 | "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi", 54 | "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always", 55 | "test:all": [ 56 | "@sniffer:check", 57 | "@stan", 58 | "@test:coverage" 59 | ], 60 | "test:coverage": [ 61 | "@putenv XDEBUG_MODE=coverage", 62 | "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --coverage-clover build/coverage/clover.xml --coverage-html build/coverage --coverage-text" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Slim coding standard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | src 16 | tests 17 | -------------------------------------------------------------------------------- /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#' -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | tests 12 | 13 | 14 | 15 | 16 | 17 | src 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/TwigExtension.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/TwigRuntimeLoader.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 | --------------------------------------------------------------------------------