Skip to content

Commit

Permalink
#11 use nelmio/alice for data-faking (#17)
Browse files Browse the repository at this point in the history
* #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]>
  • Loading branch information
davidhoelzel and bschultz authored Apr 4, 2024
1 parent 50f2c96 commit 23adb72
Show file tree
Hide file tree
Showing 34 changed files with 1,021 additions and 89 deletions.
1 change: 1 addition & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'@PHP80Migration:risky' => true,
'array_syntax' => ['syntax' => 'short'],
'yoda_style' => false,
])
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
"symfony/twig-bundle": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0"
"symfony/yaml": "^6.4|^7.0",
"nelmio/alice": "^3.13"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
Expand Down
29 changes: 26 additions & 3 deletions config/documentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,31 @@

use Qossmic\TwigDocBundle\Cache\ComponentsWarmer;
use Qossmic\TwigDocBundle\Component\ComponentItemFactory;
use Qossmic\TwigDocBundle\Component\Data\Faker;
use Qossmic\TwigDocBundle\Component\Data\Generator\FixtureGenerator;
use Qossmic\TwigDocBundle\Component\Data\Generator\NullGenerator;
use Qossmic\TwigDocBundle\Component\Data\Generator\ScalarGenerator;
use Qossmic\TwigDocBundle\Controller\TwigDocController;
use Qossmic\TwigDocBundle\Service\CategoryService;
use Qossmic\TwigDocBundle\Service\ComponentService;
use Qossmic\TwigDocBundle\Twig\TwigDocExtension;

return static function (ContainerConfigurator $container) {
$container->services()->set('twig_doc.controller.documentation', TwigDocController::class)
return static function (ContainerConfigurator $container): void {
$container->services()
->set('twig_doc.controller.documentation', TwigDocController::class)
->public()
->arg('$twig', service('twig'))
->arg('$componentService', service('twig_doc.service.component'))
->arg('$profiler', service('profiler')->nullOnInvalid())

->set('twig_doc.service.category', CategoryService::class)
->alias(CategoryService::class, 'twig_doc.service.category')

->set('twig_doc.service.component_factory', ComponentItemFactory::class)
->public()
->arg('$validator', service('validator'))
->arg('$categoryService', service('twig_doc.service.category'))
->arg('$faker', service('twig_doc.service.faker'))
->alias(ComponentItemFactory::class, 'twig_doc.service.component_factory')

->set('twig_doc.service.component', ComponentService::class)
Expand All @@ -45,5 +52,21 @@
->arg('$container', service('service_container'))
->tag('kernel.cache_warmer')
->alias(ComponentsWarmer::class, 'twig_doc.cache_warmer')
;

->set('twig_doc.service.faker', Faker::class)
->public()
->alias(Faker::class, 'twig_doc.service.faker')

->set('twig_doc.data_generator.scalar', ScalarGenerator::class)
->public()
->tag('twig_doc.data_generator', ['priority' => -5])

->set('twig_doc.data_generator.fixture', FixtureGenerator::class)
->public()
->tag('twig_doc.data_generator', ['priority' => -10])

// use null-generator as last one to ensure all other are attempted first
->set('twig_doc.data_generator.null', NullGenerator::class)
->public()
->tag('twig_doc.data_generator', ['priority' => -100]);
};
100 changes: 100 additions & 0 deletions docs/ComponentConfiguration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
### Component Configuration

1. [In Template](#in-template)
2. [In Configuration File](#config-file)
3. [Template Parameters](#parameter-provision)
4. [Custom Data Provider](#custom-data-provider)

You have two possibilities to let the bundle know of your components:

1. Directly in the template of the component itself (you should stick to this)
Expand Down Expand Up @@ -77,3 +82,98 @@ components:
- name: Button
path: '%twig.default_path%/snippets/FancyButton.html.twig'
```
### Parameter Provision
You must provide the types of your template parameters in the configuration.
As twig templates are not aware of types, there is no other possibility at the moment.
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
define the types of your parameters in the component configuration.
The bundle will take care of creating a set of parameters for every component.
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:
```twig
{#TWIG_DOC
title: Fancy Button
description: This is a really fancy button
category: Buttons
tags:
- button
parameters:
type: String
text: String
user: App\Entity\User
#TWIG_DOC}

{% if user %}
Hello {{ user.name }}
{% endif %}
<button class="btn btn-{{ type }}">{{ text }}</button>
```

As we do not provide an explicit variation, the bundle creates a default variation for this component.
This default variation will contain a fixture for the user object, as well as random values for other parameters.
If the property "name" of the user object is writable, the bundle will also create a random text-value for the name.

So, what to do if you want an example of both possibilities (user as object and as NULL)? Answer: provide variations for both cases:
```twig
{#TWIG_DOC
title: Fancy Button
description: This is a really fancy button
category: Buttons
tags:
- button
parameters:
user: App\Entity\User
type: String
text: String
variations:
logged-in:
user:
name: superadmin
type: primary
anonymous:
user: null
text: Button Text
#TWIG_DOC}
{% if user %}
Hello {{ user.name }}
{% endif %}
<button class="btn btn-{{ type }}">{{ text }}</button>
```

For all parameters that are missing from the variations configuration, the bundle will create random-values with FakerPHP.
It is possible to mix explicitly defined parameter-values and randomly created ones.

### Custom Data Provider

This bundle comes with 3 default data providers to create fake data for your components:

- FixtureGenerator
- creates fixtures for classes with nelmio/alice and fakerphp/faker
- ScalarGenerator
- creates scalar values for string/bool/number parameters in your components with fakerphp
- NullGenerator
- creates null values for any unknown type

You can easily add your own data generator by creating an implementation of `Qossmic\TwigDocBundle\Component\Data\GeneratorInterface`
and tagging it with `twig_doc.data_generator`. The higher the priority, the earlier the generator will be used.
This works by using the ["tagged_iterator" functionality](https://symfony.com/doc/current/service_container/tags.html#tagged-services-with-priority) of Symfony.
```php
#[AutoconfigureTag('twig_doc.data_generator', ['priority' => 10])]
class CustomGenerator implements GeneratorInterface
{
public function supports(string $type, mixed $context = null): bool
{
return $type === Special::class;
}

public function generate(string $type, mixed $context = null): Special
{
return new Special([
'key' => 'value',
]);
}
}
```
72 changes: 44 additions & 28 deletions src/Component/ComponentItemFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

namespace Qossmic\TwigDocBundle\Component;

use Qossmic\TwigDocBundle\Component\Data\Faker;
use Qossmic\TwigDocBundle\Exception\InvalidComponentConfigurationException;
use Qossmic\TwigDocBundle\Service\CategoryService;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;

readonly class ComponentItemFactory
{
public function __construct(private ValidatorInterface $validator, private CategoryService $categoryService)
{
public function __construct(
private ValidatorInterface $validator,
private CategoryService $categoryService,
private Faker $faker
) {
}

/**
Expand Down Expand Up @@ -48,6 +52,9 @@ public function create(array $data): ComponentItem
return $item;
}

/**
* @throws InvalidComponentConfigurationException
*/
private function createItem(array $data): ComponentItem
{
$item = new ComponentItem();
Expand All @@ -56,9 +63,9 @@ private function createItem(array $data): ComponentItem
->setDescription($data['description'] ?? '')
->setTags($data['tags'] ?? [])
->setParameters($data['parameters'] ?? [])
->setVariations($data['variations'] ?? [
'default' => $this->createVariationParameters($data['parameters'] ?? []),
])
->setVariations(
$this->parseVariations($data['variations'] ?? [], $data['parameters'] ?? [])
)
->setProjectPath($data['path'] ?? '')
->setRenderPath($data['renderPath'] ?? '');

Expand Down Expand Up @@ -87,37 +94,46 @@ public function getParamsFromVariables(array $variables): array
return $r;
}

public function createVariationParameters(array $parameters): array
/**
* @throws InvalidComponentConfigurationException
*/
private function parseVariations(?array $variations, ?array $parameters): array
{
if (!$parameters) {
return ['default' => []];
}

if (!$variations) {
return [
'default' => $this->faker->getFakeData($parameters),
];
}

$result = [];

foreach ($variations as $variationName => $variationParams) {
if (!\is_array($variationParams)) {
throw new InvalidComponentConfigurationException(ConstraintViolationList::createFromMessage(sprintf('A component variation must contain an array of parameters. Variation Name: %s', $variationName)));
}
$result[$variationName] = $this->createVariationParameters($parameters, $variationParams);
}

return $result;
}

private function createVariationParameters(array $parameters, array $variation): array
{
$params = [];

foreach ($parameters as $name => $type) {
if (\is_array($type)) {
$paramValue = $this->createVariationParameters($type);
$paramValue = $this->createVariationParameters($type, $variation[$name] ?? []);
} else {
$paramValue = $this->createParamValue($type);
$paramValue = $this->faker->getFakeData([$name => $type], $variation[$name]);
}
$params[$name] = $paramValue;
$params += $paramValue;
}

return $params;
}

private function createParamValue(string $type): bool|int|float|string|null
{
switch (strtolower($type)) {
default:
return null;
case 'string':
return 'Hello World';
case 'int':
case 'integer':
return random_int(0, 100000);
case 'bool':
case 'boolean':
return [true, false][rand(0, 1)];
case 'float':
case 'double':
return (float) rand(1, 1000) / 100;
}
}
}
4 changes: 1 addition & 3 deletions src/Component/ComponentItemList.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ static function (ComponentItem $item) use ($query) {
break;
case 'tags':
$tags = array_map('trim', explode(',', strtolower($query)));
$components = array_filter($this->getArrayCopy(), static function (ComponentItem $item) use ($tags) {
return array_intersect($tags, array_map('strtolower', $item->getTags())) !== [];
});
$components = array_filter($this->getArrayCopy(), static fn (ComponentItem $item) => array_intersect($tags, array_map('strtolower', $item->getTags())) !== []);

break;
case 'name':
Expand Down
55 changes: 55 additions & 0 deletions src/Component/Data/Faker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Qossmic\TwigDocBundle\Component\Data;

/**
* Creates fake data to be used in variation display for components.
*/
class Faker
{
public function __construct(
/**
* @param GeneratorInterface[] $generators
*/
private readonly iterable $generators
) {
}

public function getFakeData(array $params, mixed $variation = []): array
{
return $this->createFakeData($params, $variation);
}

private function createFakeData(array $params, mixed $variation): array
{
$result = [];

foreach ($params as $name => $type) {
if (\is_array($type)) {
$result[$name] = $this->createFakeData($type, $variation[$name] ?? null);

continue;
}

foreach ($this->generators as $generator) {
if (\array_key_exists($name, $result) || !$generator->supports($type)) {
continue;
}
if ($generator->supports($type, $variation)) {
$result[$name] = $generator->generate($type, $variation);

break;
}
}

if (!\array_key_exists($name, $result)) {
// set from variation
$result[$name] = $variation;
}
}

return $result;
}
}
21 changes: 21 additions & 0 deletions src/Component/Data/FixtureData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Qossmic\TwigDocBundle\Component\Data;

use Symfony\Component\PropertyInfo\Type;

/**
* @codeCoverageIgnore
*/
readonly class FixtureData
{
public function __construct(
public string $className,
/** @param array<string, Type> $properties */
public array $properties,
public array $params = []
) {
}
}
Loading

0 comments on commit 23adb72

Please sign in to comment.