Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#11 use nelmio/alice for data-faking #17

Merged
merged 16 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')

bschultz marked this conversation as resolved.
Show resolved Hide resolved
->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

bschultz marked this conversation as resolved.
Show resolved Hide resolved
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
bschultz marked this conversation as resolved.
Show resolved Hide resolved
) {
}

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

bschultz marked this conversation as resolved.
Show resolved Hide resolved
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
Loading