Skip to content

Commit 2da0d83

Browse files
weaverryankbond
andcommittedJun 16, 2021
Initial commit of symfony/ux-twig-component
Co-authored-by: Kevin Bond <[email protected]>
0 parents  commit 2da0d83

24 files changed

+972
-0
lines changed
 

‎.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.github export-ignore
2+
/tests export-ignore

‎.github/workflows/ci.yml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
tests:
9+
name: PHP ${{ matrix.php }}, SF ${{ matrix.symfony }} - ${{ matrix.stability }}
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
php: [7.2, 7.4, 8.0]
14+
stability: [hightest]
15+
symfony: [4.4.*, 5.2.*, 5.3.*]
16+
include:
17+
- php: 7.2
18+
stability: lowest
19+
symfony: '*'
20+
- php: 8.0
21+
stability: highest
22+
symfony: '5.4.*@dev'
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v2.3.3
26+
27+
- name: Setup PHP
28+
uses: shivammathur/setup-php@2.7.0
29+
with:
30+
php-version: ${{ matrix.php }}
31+
coverage: none
32+
33+
- name: Install Symfony Flex
34+
run: composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-main
35+
36+
- name: Set minimum-stability to dev
37+
run: composer config minimum-stability dev
38+
if: ${{ contains(matrix.symfony, '@dev') }}
39+
40+
- name: Install dependencies
41+
uses: ramsey/composer-install@v1
42+
with:
43+
dependency-versions: ${{ matrix.stability }}
44+
composer-options: --prefer-dist
45+
env:
46+
SYMFONY_REQUIRE: ${{ matrix.symfony }}
47+
48+
- name: Test
49+
run: vendor/bin/simple-phpunit -v

‎.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/composer.lock
2+
/phpunit.xml
3+
/vendor/
4+
/var/
5+
/.phpunit.result.cache

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2004-2021 Fabien Potencier
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 furnished
10+
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
21+
THE SOFTWARE.

‎composer.json

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "symfony/ux-twig-component",
3+
"type": "symfony-bundle",
4+
"description": "Twig components for Symfony",
5+
"keywords": [
6+
"symfony-ux",
7+
"twig",
8+
"components"
9+
],
10+
"homepage": "https://symfony.com",
11+
"license": "MIT",
12+
"authors": [
13+
{
14+
"name": "Symfony Community",
15+
"homepage": "https://symfony.com/contributors"
16+
}
17+
],
18+
"autoload": {
19+
"psr-4": {
20+
"Symfony\\UX\\TwigComponent\\": "src/"
21+
}
22+
},
23+
"autoload-dev": {
24+
"psr-4": {
25+
"Symfony\\UX\\TwigComponent\\Tests\\": "tests/"
26+
}
27+
},
28+
"require": {
29+
"php": ">=7.2.5",
30+
"twig/twig": "^2.0|^3.0",
31+
"symfony/property-access": "^4.4|^5.0",
32+
"symfony/dependency-injection": "^4.4|^5.0"
33+
},
34+
"require-dev": {
35+
"symfony/framework-bundle": "^4.4|^5.0",
36+
"symfony/twig-bundle": "^4.4|^5.0",
37+
"symfony/phpunit-bridge": "^5.2"
38+
},
39+
"conflict": {
40+
"symfony/dependency-injection": "<4.4.18,<5.1.10,<5.2.1"
41+
},
42+
"extra": {
43+
"thanks": {
44+
"name": "symfony/ux",
45+
"url": "https://github.com/symfony/ux"
46+
}
47+
}
48+
}

‎phpunit.xml.dist

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
4+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
6+
colors="true"
7+
bootstrap="vendor/autoload.php"
8+
failOnRisky="true"
9+
failOnWarning="true"
10+
>
11+
<php>
12+
<ini name="error_reporting" value="-1" />
13+
<server name="KERNEL_CLASS" value="Symfony\UX\TwigComponent\Tests\Fixture\Kernel" />
14+
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
15+
</php>
16+
17+
<testsuites>
18+
<testsuite name="symfony/ux-twig-component Test Suite">
19+
<directory>./tests/</directory>
20+
</testsuite>
21+
</testsuites>
22+
23+
<filter>
24+
<whitelist processUncoveredFilesFromWhitelist="true">
25+
<directory suffix=".php">./src</directory>
26+
</whitelist>
27+
</filter>
28+
29+
<listeners>
30+
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
31+
</listeners>
32+
</phpunit>

‎src/ComponentFactory.php

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent;
13+
14+
use Symfony\Component\DependencyInjection\ServiceLocator;
15+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
16+
17+
/**
18+
* @author Kevin Bond <kevinbond@gmail.com>
19+
*
20+
* @experimental
21+
*/
22+
final class ComponentFactory
23+
{
24+
private $components;
25+
private $propertyAccessor;
26+
private $serviceIdMap;
27+
28+
/**
29+
* @param ServiceLocator|ComponentInterface[] $components
30+
*/
31+
public function __construct(ServiceLocator $components, PropertyAccessorInterface $propertyAccessor, array $serviceIdMap)
32+
{
33+
$this->components = $components;
34+
$this->propertyAccessor = $propertyAccessor;
35+
$this->serviceIdMap = $serviceIdMap;
36+
}
37+
38+
/**
39+
* Creates the component and "mounts" it with the passed data.
40+
*/
41+
public function create(string $name, array $data = []): ComponentInterface
42+
{
43+
$component = $this->getComponent($name);
44+
45+
$this->mount($component, $data);
46+
47+
// set data that wasn't set in mount on the component directly
48+
foreach ($data as $property => $value) {
49+
if (!$this->propertyAccessor->isWritable($component, $property)) {
50+
throw new \LogicException(\sprintf('Unable to write "%s" to component "%s". Make sure this is a writable property or create a mount() with a $%s argument.', $property, \get_class($component), $property));
51+
}
52+
53+
$this->propertyAccessor->setValue($component, $property, $value);
54+
}
55+
56+
return $component;
57+
}
58+
59+
/**
60+
* Returns the "unmounted" component.
61+
*/
62+
public function get(string $name): ComponentInterface
63+
{
64+
return $this->getComponent($name);
65+
}
66+
67+
public function serviceIdFor(string $name): string
68+
{
69+
if (!isset($this->serviceIdMap[$name])) {
70+
throw new \InvalidArgumentException('Component not found.');
71+
}
72+
73+
return $this->serviceIdMap[$name];
74+
}
75+
76+
private function mount(ComponentInterface $component, array &$data): void
77+
{
78+
try {
79+
$method = (new \ReflectionClass($component))->getMethod('mount');
80+
} catch (\ReflectionException $e) {
81+
// no hydrate method
82+
return;
83+
}
84+
85+
$parameters = [];
86+
87+
foreach ($method->getParameters() as $refParameter) {
88+
$name = $refParameter->getName();
89+
90+
if (\array_key_exists($name, $data)) {
91+
$parameters[] = $data[$name];
92+
93+
// remove the data element so it isn't used to set the property directly.
94+
unset($data[$name]);
95+
} elseif ($refParameter->isDefaultValueAvailable()) {
96+
$parameters[] = $refParameter->getDefaultValue();
97+
} else {
98+
throw new \LogicException(\sprintf('%s::mount() has a required $%s parameter. Make sure this is passed or make give a default value.', \get_class($component), $refParameter->getName()));
99+
}
100+
}
101+
102+
$component->mount(...$parameters);
103+
}
104+
105+
private function getComponent(string $name): ComponentInterface
106+
{
107+
if (!$this->components->has($name)) {
108+
throw new \InvalidArgumentException(sprintf(
109+
'Unknown component "%s". The registered components are: %s',
110+
$name,
111+
implode(', ', array_keys($this->serviceIdMap))
112+
));
113+
}
114+
115+
return $this->components->get($name);
116+
}
117+
}

‎src/ComponentInterface.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*
17+
* @experimental
18+
*/
19+
interface ComponentInterface
20+
{
21+
public static function getComponentName(): string;
22+
}

‎src/ComponentRenderer.php

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent;
13+
14+
use Twig\Environment;
15+
16+
/**
17+
* @author Kevin Bond <kevinbond@gmail.com>
18+
*
19+
* @experimental
20+
*/
21+
final class ComponentRenderer
22+
{
23+
private $twig;
24+
25+
public function __construct(Environment $twig)
26+
{
27+
$this->twig = $twig;
28+
}
29+
30+
public function render(ComponentInterface $component): string
31+
{
32+
// TODO: Template attribute/annotation/interface to customize
33+
// TODO: Self-Rendering components?
34+
$templateName = \sprintf('components/%s.html.twig', $component::getComponentName());
35+
36+
return $this->twig->render($templateName, ['this' => $component]);
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\LogicException;
17+
use Symfony\UX\TwigComponent\ComponentFactory;
18+
19+
/**
20+
* @author Kevin Bond <kevinbond@gmail.com>
21+
*
22+
* @experimental
23+
*/
24+
final class TwigComponentPass implements CompilerPassInterface
25+
{
26+
public function process(ContainerBuilder $container): void
27+
{
28+
$serviceIdMap = [];
29+
30+
foreach (\array_keys($container->findTaggedServiceIds('twig.component')) as $serviceId) {
31+
$definition = $container->getDefinition($serviceId);
32+
33+
// make all component services non-shared
34+
$definition->setShared(false);
35+
36+
$name = $definition->getClass()::getComponentName();
37+
38+
// ensure component not already defined
39+
if (\array_key_exists($name, $serviceIdMap)) {
40+
throw new LogicException(\sprintf('Component "%s" is already registered as "%s", components cannot be registered more than once.', $definition->getClass(), $serviceIdMap[$name]));
41+
}
42+
43+
// add to service id map for ComponentFactory
44+
$serviceIdMap[$name] = $serviceId;
45+
}
46+
47+
$container->getDefinition(ComponentFactory::class)->setArgument(2, $serviceIdMap);
48+
}
49+
}

0 commit comments

Comments
 (0)
Please sign in to comment.