Skip to content

Commit fb53c8e

Browse files
committed
🎉 Inits repository
0 parents  commit fb53c8e

9 files changed

+351
-0
lines changed

.editorconfig

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# This file is for unifying the coding style for different editors and IDEs
2+
# editorconfig.org
3+
4+
root = true
5+
6+
[*]
7+
indent_size = 2
8+
end_of_line = lf
9+
indent_style = space
10+
charset = utf-8
11+
trim_trailing_whitespace = true
12+
insert_final_newline = true
13+
14+
# PHP PSR-12 Coding Standards
15+
# http://www.php-fig.org/psr/psr-12/
16+
17+
[*.php]
18+
indent_size = 4
19+
20+
[*.md]
21+
trim_trailing_whitespace = false

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/composer.lock
2+
/vendor

.phpcs.xml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0"?>
2+
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PHP_CodeSniffer" xsi:noNamespaceSchemaLocation="phpcs.xsd">
3+
<!-- Exclude the Composer Vendor directory. -->
4+
<exclude-pattern>/vendor/*</exclude-pattern>
5+
6+
<!-- Include the PSR-12 Coding Standard -->
7+
<rule ref="PSR12" />
8+
9+
<!-- Add in some extra rules from other standards. -->
10+
<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter" />
11+
<rule ref="Generic.Commenting.Todo" />
12+
</ruleset>

CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [1.0.0] - 2022-01-05
11+
12+
* Adds the component handle loader
13+
14+
[Unreleased]: https://github.com/gglnx/twig-component-handle-loader/compare/v1.0.0...HEAD
15+
[1.0.0]: https://github.com/gglnx/twig-component-handle-loader/releases/tag/v1.0.0

LICENSE

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright 2022 Dennis Morhardt, https://dennismorhardt.de
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be
12+
included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Component Handle Loader for Twig
2+
3+
[![Packagist](https://img.shields.io/packagist/v/gglnx/twig-component-handle-loader.svg)](https://packagist.org/packages/gglnx/twig-component-handle-loader)
4+
5+
[Twig](https://twig.symfony.com/) loader for loading templates by using a component handle based on [the Fractal naming convention](https://fractal.build/guide/core-concepts/naming.html#referencing-other-items).
6+
7+
For example:
8+
9+
```
10+
├── components
11+
│ └── small-components
12+
| └── button
13+
| └── button.twig # Will be @button
14+
```
15+
16+
You can now include the button template with the `@[component-handle]` syntax:
17+
18+
```twig
19+
{% include '@button' %}
20+
```
21+
22+
## Requirements
23+
24+
* Twig >=2.14
25+
* PHP >=7.4
26+
27+
## Installation
28+
29+
The recommended way to install this loader is via [Composer](https://getcomposer.org/):
30+
31+
```bash
32+
composer require gglnx/twig-component-handle-loader
33+
```
34+
35+
Then you can use this loader directly with Twig:
36+
37+
```php
38+
require_once '/path/to/vendor/autoload.php';
39+
40+
$loader = new \Gglnx\TwigComponentHandleLoader\Loader\TwigComponentHandleLoader('../path-to-my-components');
41+
$twig = new \Twig\Environment($loader);
42+
```
43+
44+
You can also combine this loader with other loaders using [`ChainLoader`](https://twig.symfony.com/doc/3.x/api.html#twig-loader-chainloader).
45+
46+
## Differences between this loader and the Fractal implementation
47+
48+
* [Ordering and hiding](https://fractal.build/guide/core-concepts/naming.html#ordering-and-hiding) are yet not fully supported and tested.
49+
* Using [Prefixes](https://fractal.build/guide/core-concepts/naming.html#prefixes) is not possible.
50+
* Overriding [handle in the component configuration](https://fractal.build/guide/components/configuration-reference.html#component-properties) is not possible.
51+
52+
Prefixing and overriding the component handle require to read the corresponding component configuration. This could be out-of-scope for this loader and maybe better placed in a specific Twig Fractal loader.

composer.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "gglnx/twig-component-handle-loader",
3+
"description": "Load Twig templates using Fractal-style component handles",
4+
"type": "library",
5+
"keywords": ["twig", "twig-loader"],
6+
"version": "1.0.0",
7+
"license": "MIT",
8+
"authors": [
9+
{
10+
"name": "Dennis Morhardt",
11+
"email": "[email protected]",
12+
"homepage": "https://dennismorhardt.de/"
13+
}
14+
],
15+
"support": {
16+
"issues": "https://github.com/gglnx/twig-component-handle-loader/issues?state=open",
17+
"source": "https://github.com/gglnx/twig-component-handle-loader",
18+
"docs": "https://github.com/gglnx/twig-component-handle-loader"
19+
},
20+
"require": {
21+
"php": ">=7.2.5",
22+
"twig/twig": "^2.14"
23+
},
24+
"require-dev": {
25+
"squizlabs/php_codesniffer": "^3.6"
26+
},
27+
"config": {
28+
"sort-packages": true
29+
},
30+
"autoload": {
31+
"psr-4": {
32+
"Gglnx\\TwigComponentHandleLoader\\": "src/"
33+
}
34+
}
35+
}

src/Loader/ComponentHandleLoader.php

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?php
2+
3+
/**
4+
* Twig Component Handle Loader
5+
*
6+
* @copyright 2022 Dennis Morhardt <[email protected]>
7+
* @license MIT License; see LICENSE file for details.
8+
*/
9+
10+
namespace Gglnx\TwigComponentHandleLoader\Loader;
11+
12+
use RecursiveCallbackFilterIterator;
13+
use RecursiveDirectoryIterator;
14+
use RecursiveIteratorIterator;
15+
use Twig\Error\LoaderError;
16+
use Twig\Loader\LoaderInterface;
17+
use Twig\Source;
18+
19+
/**
20+
* Add support for loading templates with '@component' handle syntax (like Fractal).
21+
*
22+
* @author Dennis Morhardt <[email protected]>
23+
* @package Gglnx\TwigComponentHandleLoader\Loader
24+
*/
25+
class ComponentHandleLoader implements LoaderInterface
26+
{
27+
/**
28+
* A path where to look for components.
29+
*
30+
* @var string
31+
*/
32+
protected $pathToComponents;
33+
34+
/**
35+
* List of all components
36+
*
37+
* @var array
38+
*/
39+
private $components = [];
40+
41+
/**
42+
* Inits this loader
43+
*
44+
* @param string $pathToComponents
45+
* @return void
46+
*/
47+
public function __construct(string $pathToComponents)
48+
{
49+
$this->pathToComponents = $pathToComponents;
50+
$this->components = $this->getComponents();
51+
}
52+
53+
/**
54+
* @inheritdoc
55+
*/
56+
public function getSourceContext($name)
57+
{
58+
$path = $this->findTemplate($name);
59+
60+
return new Source(file_get_contents($path, true), $name, $path);
61+
}
62+
63+
/**
64+
* @inheritdoc
65+
*/
66+
public function getCacheKey($name)
67+
{
68+
$path = $this->findTemplate($name);
69+
$len = strlen($this->pathToComponents);
70+
71+
if (strncmp($this->pathToComponents, $path, $len) === 0) {
72+
return substr($path, $len);
73+
}
74+
75+
return $path;
76+
}
77+
78+
/**
79+
* @inheritdoc
80+
*/
81+
public function isFresh($name, $time)
82+
{
83+
$path = $this->findTemplate($name);
84+
85+
return filemtime($path) < $time;
86+
}
87+
88+
/**
89+
* @inheritdoc
90+
*/
91+
public function exists($name)
92+
{
93+
return $this->findTemplate($name, false) !== null;
94+
}
95+
96+
/**
97+
* Finds a template by at-name
98+
*
99+
* @param string $name The template logical name.
100+
* @param bool $throw Throw on errors.
101+
* @throws LoaderError When $name is not found or doesn't start with @.
102+
* @return string|null
103+
*/
104+
protected function findTemplate($name, $throw = true)
105+
{
106+
try {
107+
// Throw error if name doesn't start with '@'
108+
if ($name[0] !== '@') {
109+
throw new LoaderError(sprintf(
110+
'Template name "%s" does not support with @ as needed for component handle.',
111+
$name
112+
));
113+
}
114+
115+
// Throw error if name includes a slash (we don't support namespaces)
116+
if (strpos($name, '/') !== false) {
117+
throw new LoaderError(sprintf(
118+
'Template name "%s" includes an /, but namespaced are not supported.',
119+
$name
120+
));
121+
}
122+
123+
// Try to find component
124+
if (!isset($this->components[$name])) {
125+
throw new LoaderError(sprintf(
126+
'Unable to find component "%s" (looked into: %s).',
127+
$name,
128+
$this->pathToComponents
129+
));
130+
}
131+
} catch (LoaderError $exception) {
132+
if (!$throw) {
133+
return null;
134+
}
135+
136+
throw $exception;
137+
}
138+
139+
// Get path to component
140+
return $this->components[$name];
141+
}
142+
143+
/**
144+
* Loads all component paths
145+
*
146+
* @throws LoaderError When path to components doesn't exits.
147+
* @return array
148+
*/
149+
private function getComponents()
150+
{
151+
// Check path to components
152+
if (!is_dir($this->pathToComponents)) {
153+
throw new LoaderError(sprintf(
154+
'The "%s" directory does not exist.',
155+
$this->pathToComponents
156+
));
157+
}
158+
159+
// Parse through all component folders
160+
$directory = new RecursiveDirectoryIterator($this->pathToComponents);
161+
162+
// Build filter for 'twig' files
163+
$files = new RecursiveCallbackFilterIterator(
164+
$directory,
165+
function ($current, $key, $iterator) {
166+
if ($iterator->hasChildren()) {
167+
return true;
168+
}
169+
170+
if (!$current->isFile()) {
171+
return false;
172+
}
173+
174+
if (pathinfo($key, PATHINFO_EXTENSION) !== 'twig') {
175+
return false;
176+
}
177+
178+
return true;
179+
}
180+
);
181+
182+
// Get all matches ending with 'twig'
183+
$matches = array_map(
184+
function ($match) {
185+
return sprintf('@%s', pathinfo($match->getFilename(), PATHINFO_FILENAME));
186+
},
187+
iterator_to_array(new RecursiveIteratorIterator($files))
188+
);
189+
190+
// Return all components
191+
return array_flip($matches);
192+
}
193+
}

0 commit comments

Comments
 (0)