Skip to content

Commit

Permalink
Add TextTypeDefaultStringExtension with associated configuration setting
Browse files Browse the repository at this point in the history
  • Loading branch information
benr77 committed Jun 28, 2024
1 parent de84f31 commit 102d304
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 3 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ return [
## Features

- [Rate Limiter Attributes](#rate-limiter-attributes)
- [Form Text Fields Empty String](#form-text-fields-empty-string)

### Rate Limiter Attributes

Expand Down Expand Up @@ -82,6 +83,24 @@ headsnet_symfony_tools:
Thanks to [this JoliCode article](https://jolicode.com/blog/rate-limit-your-symfony-apis) for the inspiration!
### Form Text Fields Empty String
By default Symfony uses `null` as the default value for text-based form fields. This results in `null` values being all
over the codebase.

An easy way to fix this is to change the default behaviour so text-based fields return an empty string
`''` instead of `null`. Then, class properties can be typed `string` instead of `string|null` and this
can eliminate a lot of null checks in the client code.

This is an opinionated solution, so must be enabled in the bundle configuration:

```yaml
headsnet_symfony_tools:
forms:
default_empty_string:
enabled: true
```

## License

Released under the [MIT License](LICENSE).
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"symfony/config": "^6.0 || ^7.0",
"symfony/dependency-injection": "^6.0 || ^7.0",
"symfony/event-dispatcher": "^6.0 || ^7.0",
"symfony/form": "^6.0 || ^7.0",
"symfony/http-kernel": "^6.0 || ^7.0",
"symfony/lock": "^6.0 || ^7.0",
"symfony/rate-limiter": "^6.0 || ^7.0"
Expand Down
34 changes: 34 additions & 0 deletions src/Form/Extension/TextTypeDefaultStringExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);

namespace Headsnet\SymfonyToolsBundle\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Override the default behaviour of TextType, which returns null when the field is empty.
*
* To keep our entities free of nulls, we want to return an empty string instead. This
* extension simply sets the "empty_data" option to "" for all TextType fields.
*/
final class TextTypeDefaultStringExtension extends AbstractTypeExtension
{
#[\Override]
public static function getExtendedTypes(): iterable
{
return [TextType::class, EmailType::class, UrlType::class, TextareaType::class];
}

#[\Override]
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);

$resolver->setDefault('empty_data', '');
}
}
23 changes: 23 additions & 0 deletions src/HeadsnetSymfonyToolsBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Headsnet\SymfonyToolsBundle;

use Headsnet\SymfonyToolsBundle\Form\Extension\TextTypeDefaultStringExtension;
use Headsnet\SymfonyToolsBundle\RateLimiting\RateLimitingCompilerPass;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -20,13 +21,26 @@ public function configure(DefinitionConfigurator $definition): void
->booleanNode('use_headers')->end()
->end()
->end() // End rate_limiting
->arrayNode('forms')
->addDefaultsIfNotSet()
->children()
->arrayNode('default_empty_string')
->canBeEnabled()
->end() // End default_empty_string
->end()
->end() // End forms
->end()
;
}

/**
* @param array{
* root_namespace: string,
* forms: array{
* default_empty_string: array{
* enabled: bool
* }
* },
* rate_limiting: array{use_headers: bool}
* } $config
*/
Expand All @@ -38,6 +52,15 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
->set('headsnet_symfony_tools.root_namespace', $config['root_namespace'])
->set('headsnet_symfony_tools.rate_limiting.use_headers', $config['rate_limiting']['use_headers'])
;

if ($config['forms']['default_empty_string']['enabled']) {
$container->services()
->set('headsnet_symfony_tools.forms.default_empty_string_extension')
->class(TextTypeDefaultStringExtension::class)
->tag('form.type_extension')
->public()
;
}
}

public function build(ContainerBuilder $container): void
Expand Down
9 changes: 6 additions & 3 deletions tests/Fixtures/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
headsnet_symfony_tools:
root_namespace: App
rate_limiting:
use_headers: true
root_namespace: App
forms:
default_empty_string:
enabled: true
rate_limiting:
use_headers: true

43 changes: 43 additions & 0 deletions tests/Form/Extension/TextTypeDefaultStringExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);

namespace Headsnet\SymfonyToolsBundle\Tests\Form\Extension;

use Headsnet\SymfonyToolsBundle\Form\Extension\TextTypeDefaultStringExtension;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\OptionsResolver\OptionsResolver;

#[CoversClass(TextTypeDefaultStringExtension::class)]
final class TextTypeDefaultStringExtensionTest extends TestCase
{
#[Test]
public function applies_to_all_textual_form_types(): void
{
$result = TextTypeDefaultStringExtension::getExtendedTypes();

$this->assertEquals([TextType::class, EmailType::class, UrlType::class, TextareaType::class], $result);
}

#[Test]
public function validation_is_disabled(): void
{
$optionsResolver = new OptionsResolver();
$sut = new TextTypeDefaultStringExtension();

$sut->configureOptions($optionsResolver);

$this->assertTrue($optionsResolver->isDefined('empty_data'));
$this->assertEquals(
[
'empty_data' => '',
],
$optionsResolver->resolve()
);
}
}
4 changes: 4 additions & 0 deletions tests/HeadsnetSymfonyToolsBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@ public function initialise_bundle(): void
$this->assertTrue(
$container->hasParameter('headsnet_symfony_tools.rate_limiting.use_headers')
);

$this->assertNotNull(
$container->get('headsnet_symfony_tools.forms.default_empty_string_extension')
);
}
}

0 comments on commit 102d304

Please sign in to comment.