Skip to content

Commit 23adb72

Browse files
davidhoelzelBenjamin Schultz
andauthored
#11 use nelmio/alice for data-faking (#17)
* #11 use nelmio/alice for data-faking * #11 cs-fixer * #11 implemented generators for providing template data * Update docs/ComponentConfiguration.md Co-authored-by: Benjamin Schultz <[email protected]> * Update tests/Unit/Component/ComponentItemFactoryTest.php Co-authored-by: Benjamin Schultz <[email protected]> * Breakpoint configuration (#18) * add breakpoint config * add disclaimer - added forgotten documentation for breakpoints * Fix typo * Optimize function calls [EA] '$item->$method()' would make more sense here (it's also faster). * Fix route prefix Regarding best practices, the bundle routes must be prefixed with the bundle alias. * Cleanup and optimize code Add strict types and return types where it was missing. * merge update * add class usages * #11 code review changes - added cs-fixer rule for strict types * #11 code review changes --------- Co-authored-by: Benjamin Schultz <[email protected]>
1 parent 50f2c96 commit 23adb72

34 files changed

+1021
-89
lines changed

.php-cs-fixer.dist.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
->setRules([
1111
'@Symfony' => true,
1212
'@Symfony:risky' => true,
13+
'@PHP80Migration:risky' => true,
1314
'array_syntax' => ['syntax' => 'short'],
1415
'yoda_style' => false,
1516
])

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"symfony/framework-bundle": "^6.4|^7.0",
2020
"symfony/validator": "^6.4|^7.0",
2121
"symfony/twig-bundle": "^6.4|^7.0",
22-
"symfony/yaml": "^6.4|^7.0"
22+
"symfony/yaml": "^6.4|^7.0",
23+
"nelmio/alice": "^3.13"
2324
},
2425
"require-dev": {
2526
"phpunit/phpunit": "^10.5",

config/documentation.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,31 @@
66

77
use Qossmic\TwigDocBundle\Cache\ComponentsWarmer;
88
use Qossmic\TwigDocBundle\Component\ComponentItemFactory;
9+
use Qossmic\TwigDocBundle\Component\Data\Faker;
10+
use Qossmic\TwigDocBundle\Component\Data\Generator\FixtureGenerator;
11+
use Qossmic\TwigDocBundle\Component\Data\Generator\NullGenerator;
12+
use Qossmic\TwigDocBundle\Component\Data\Generator\ScalarGenerator;
913
use Qossmic\TwigDocBundle\Controller\TwigDocController;
1014
use Qossmic\TwigDocBundle\Service\CategoryService;
1115
use Qossmic\TwigDocBundle\Service\ComponentService;
1216
use Qossmic\TwigDocBundle\Twig\TwigDocExtension;
1317

14-
return static function (ContainerConfigurator $container) {
15-
$container->services()->set('twig_doc.controller.documentation', TwigDocController::class)
18+
return static function (ContainerConfigurator $container): void {
19+
$container->services()
20+
->set('twig_doc.controller.documentation', TwigDocController::class)
1621
->public()
1722
->arg('$twig', service('twig'))
1823
->arg('$componentService', service('twig_doc.service.component'))
1924
->arg('$profiler', service('profiler')->nullOnInvalid())
25+
2026
->set('twig_doc.service.category', CategoryService::class)
2127
->alias(CategoryService::class, 'twig_doc.service.category')
2228

2329
->set('twig_doc.service.component_factory', ComponentItemFactory::class)
2430
->public()
2531
->arg('$validator', service('validator'))
2632
->arg('$categoryService', service('twig_doc.service.category'))
33+
->arg('$faker', service('twig_doc.service.faker'))
2734
->alias(ComponentItemFactory::class, 'twig_doc.service.component_factory')
2835

2936
->set('twig_doc.service.component', ComponentService::class)
@@ -45,5 +52,21 @@
4552
->arg('$container', service('service_container'))
4653
->tag('kernel.cache_warmer')
4754
->alias(ComponentsWarmer::class, 'twig_doc.cache_warmer')
48-
;
55+
56+
->set('twig_doc.service.faker', Faker::class)
57+
->public()
58+
->alias(Faker::class, 'twig_doc.service.faker')
59+
60+
->set('twig_doc.data_generator.scalar', ScalarGenerator::class)
61+
->public()
62+
->tag('twig_doc.data_generator', ['priority' => -5])
63+
64+
->set('twig_doc.data_generator.fixture', FixtureGenerator::class)
65+
->public()
66+
->tag('twig_doc.data_generator', ['priority' => -10])
67+
68+
// use null-generator as last one to ensure all other are attempted first
69+
->set('twig_doc.data_generator.null', NullGenerator::class)
70+
->public()
71+
->tag('twig_doc.data_generator', ['priority' => -100]);
4972
};

docs/ComponentConfiguration.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
### Component Configuration
22

3+
1. [In Template](#in-template)
4+
2. [In Configuration File](#config-file)
5+
3. [Template Parameters](#parameter-provision)
6+
4. [Custom Data Provider](#custom-data-provider)
7+
38
You have two possibilities to let the bundle know of your components:
49

510
1. Directly in the template of the component itself (you should stick to this)
@@ -77,3 +82,98 @@ components:
7782
- name: Button
7883
path: '%twig.default_path%/snippets/FancyButton.html.twig'
7984
```
85+
86+
### Parameter Provision
87+
88+
You must provide the types of your template parameters in the configuration.
89+
As twig templates are not aware of types, there is no other possibility at the moment.
90+
As this bundle makes use of [Nelmio/Alice](https://github.com/nelmio/alice) and [FakerPhp](https://fakerphp.github.io), all you need to do is
91+
define the types of your parameters in the component configuration.
92+
The bundle will take care of creating a set of parameters for every component.
93+
94+
E.g. when your template optionally requires a User object, you can say the template needs a parameter named user that is of type App\Entity\User:
95+
```twig
96+
{#TWIG_DOC
97+
title: Fancy Button
98+
description: This is a really fancy button
99+
category: Buttons
100+
tags:
101+
- button
102+
parameters:
103+
type: String
104+
text: String
105+
user: App\Entity\User
106+
#TWIG_DOC}
107+
108+
{% if user %}
109+
Hello {{ user.name }}
110+
{% endif %}
111+
<button class="btn btn-{{ type }}">{{ text }}</button>
112+
```
113+
114+
As we do not provide an explicit variation, the bundle creates a default variation for this component.
115+
This default variation will contain a fixture for the user object, as well as random values for other parameters.
116+
If the property "name" of the user object is writable, the bundle will also create a random text-value for the name.
117+
118+
So, what to do if you want an example of both possibilities (user as object and as NULL)? Answer: provide variations for both cases:
119+
```twig
120+
{#TWIG_DOC
121+
title: Fancy Button
122+
description: This is a really fancy button
123+
category: Buttons
124+
tags:
125+
- button
126+
parameters:
127+
user: App\Entity\User
128+
type: String
129+
text: String
130+
variations:
131+
logged-in:
132+
user:
133+
name: superadmin
134+
type: primary
135+
anonymous:
136+
user: null
137+
text: Button Text
138+
#TWIG_DOC}
139+
140+
{% if user %}
141+
Hello {{ user.name }}
142+
{% endif %}
143+
<button class="btn btn-{{ type }}">{{ text }}</button>
144+
```
145+
146+
For all parameters that are missing from the variations configuration, the bundle will create random-values with FakerPHP.
147+
It is possible to mix explicitly defined parameter-values and randomly created ones.
148+
149+
### Custom Data Provider
150+
151+
This bundle comes with 3 default data providers to create fake data for your components:
152+
153+
- FixtureGenerator
154+
- creates fixtures for classes with nelmio/alice and fakerphp/faker
155+
- ScalarGenerator
156+
- creates scalar values for string/bool/number parameters in your components with fakerphp
157+
- NullGenerator
158+
- creates null values for any unknown type
159+
160+
You can easily add your own data generator by creating an implementation of `Qossmic\TwigDocBundle\Component\Data\GeneratorInterface`
161+
and tagging it with `twig_doc.data_generator`. The higher the priority, the earlier the generator will be used.
162+
This works by using the ["tagged_iterator" functionality](https://symfony.com/doc/current/service_container/tags.html#tagged-services-with-priority) of Symfony.
163+
```php
164+
#[AutoconfigureTag('twig_doc.data_generator', ['priority' => 10])]
165+
class CustomGenerator implements GeneratorInterface
166+
{
167+
public function supports(string $type, mixed $context = null): bool
168+
{
169+
return $type === Special::class;
170+
}
171+
172+
public function generate(string $type, mixed $context = null): Special
173+
{
174+
return new Special([
175+
'key' => 'value',
176+
]);
177+
}
178+
}
179+
```

src/Component/ComponentItemFactory.php

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
namespace Qossmic\TwigDocBundle\Component;
66

7+
use Qossmic\TwigDocBundle\Component\Data\Faker;
78
use Qossmic\TwigDocBundle\Exception\InvalidComponentConfigurationException;
89
use Qossmic\TwigDocBundle\Service\CategoryService;
910
use Symfony\Component\Validator\ConstraintViolationList;
1011
use Symfony\Component\Validator\Validator\ValidatorInterface;
1112

1213
readonly class ComponentItemFactory
1314
{
14-
public function __construct(private ValidatorInterface $validator, private CategoryService $categoryService)
15-
{
15+
public function __construct(
16+
private ValidatorInterface $validator,
17+
private CategoryService $categoryService,
18+
private Faker $faker
19+
) {
1620
}
1721

1822
/**
@@ -48,6 +52,9 @@ public function create(array $data): ComponentItem
4852
return $item;
4953
}
5054

55+
/**
56+
* @throws InvalidComponentConfigurationException
57+
*/
5158
private function createItem(array $data): ComponentItem
5259
{
5360
$item = new ComponentItem();
@@ -56,9 +63,9 @@ private function createItem(array $data): ComponentItem
5663
->setDescription($data['description'] ?? '')
5764
->setTags($data['tags'] ?? [])
5865
->setParameters($data['parameters'] ?? [])
59-
->setVariations($data['variations'] ?? [
60-
'default' => $this->createVariationParameters($data['parameters'] ?? []),
61-
])
66+
->setVariations(
67+
$this->parseVariations($data['variations'] ?? [], $data['parameters'] ?? [])
68+
)
6269
->setProjectPath($data['path'] ?? '')
6370
->setRenderPath($data['renderPath'] ?? '');
6471

@@ -87,37 +94,46 @@ public function getParamsFromVariables(array $variables): array
8794
return $r;
8895
}
8996

90-
public function createVariationParameters(array $parameters): array
97+
/**
98+
* @throws InvalidComponentConfigurationException
99+
*/
100+
private function parseVariations(?array $variations, ?array $parameters): array
101+
{
102+
if (!$parameters) {
103+
return ['default' => []];
104+
}
105+
106+
if (!$variations) {
107+
return [
108+
'default' => $this->faker->getFakeData($parameters),
109+
];
110+
}
111+
112+
$result = [];
113+
114+
foreach ($variations as $variationName => $variationParams) {
115+
if (!\is_array($variationParams)) {
116+
throw new InvalidComponentConfigurationException(ConstraintViolationList::createFromMessage(sprintf('A component variation must contain an array of parameters. Variation Name: %s', $variationName)));
117+
}
118+
$result[$variationName] = $this->createVariationParameters($parameters, $variationParams);
119+
}
120+
121+
return $result;
122+
}
123+
124+
private function createVariationParameters(array $parameters, array $variation): array
91125
{
92126
$params = [];
127+
93128
foreach ($parameters as $name => $type) {
94129
if (\is_array($type)) {
95-
$paramValue = $this->createVariationParameters($type);
130+
$paramValue = $this->createVariationParameters($type, $variation[$name] ?? []);
96131
} else {
97-
$paramValue = $this->createParamValue($type);
132+
$paramValue = $this->faker->getFakeData([$name => $type], $variation[$name]);
98133
}
99-
$params[$name] = $paramValue;
134+
$params += $paramValue;
100135
}
101136

102137
return $params;
103138
}
104-
105-
private function createParamValue(string $type): bool|int|float|string|null
106-
{
107-
switch (strtolower($type)) {
108-
default:
109-
return null;
110-
case 'string':
111-
return 'Hello World';
112-
case 'int':
113-
case 'integer':
114-
return random_int(0, 100000);
115-
case 'bool':
116-
case 'boolean':
117-
return [true, false][rand(0, 1)];
118-
case 'float':
119-
case 'double':
120-
return (float) rand(1, 1000) / 100;
121-
}
122-
}
123139
}

src/Component/ComponentItemList.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ static function (ComponentItem $item) use ($query) {
8383
break;
8484
case 'tags':
8585
$tags = array_map('trim', explode(',', strtolower($query)));
86-
$components = array_filter($this->getArrayCopy(), static function (ComponentItem $item) use ($tags) {
87-
return array_intersect($tags, array_map('strtolower', $item->getTags())) !== [];
88-
});
86+
$components = array_filter($this->getArrayCopy(), static fn (ComponentItem $item) => array_intersect($tags, array_map('strtolower', $item->getTags())) !== []);
8987

9088
break;
9189
case 'name':

src/Component/Data/Faker.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Qossmic\TwigDocBundle\Component\Data;
6+
7+
/**
8+
* Creates fake data to be used in variation display for components.
9+
*/
10+
class Faker
11+
{
12+
public function __construct(
13+
/**
14+
* @param GeneratorInterface[] $generators
15+
*/
16+
private readonly iterable $generators
17+
) {
18+
}
19+
20+
public function getFakeData(array $params, mixed $variation = []): array
21+
{
22+
return $this->createFakeData($params, $variation);
23+
}
24+
25+
private function createFakeData(array $params, mixed $variation): array
26+
{
27+
$result = [];
28+
29+
foreach ($params as $name => $type) {
30+
if (\is_array($type)) {
31+
$result[$name] = $this->createFakeData($type, $variation[$name] ?? null);
32+
33+
continue;
34+
}
35+
36+
foreach ($this->generators as $generator) {
37+
if (\array_key_exists($name, $result) || !$generator->supports($type)) {
38+
continue;
39+
}
40+
if ($generator->supports($type, $variation)) {
41+
$result[$name] = $generator->generate($type, $variation);
42+
43+
break;
44+
}
45+
}
46+
47+
if (!\array_key_exists($name, $result)) {
48+
// set from variation
49+
$result[$name] = $variation;
50+
}
51+
}
52+
53+
return $result;
54+
}
55+
}

src/Component/Data/FixtureData.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Qossmic\TwigDocBundle\Component\Data;
6+
7+
use Symfony\Component\PropertyInfo\Type;
8+
9+
/**
10+
* @codeCoverageIgnore
11+
*/
12+
readonly class FixtureData
13+
{
14+
public function __construct(
15+
public string $className,
16+
/** @param array<string, Type> $properties */
17+
public array $properties,
18+
public array $params = []
19+
) {
20+
}
21+
}

0 commit comments

Comments
 (0)