From b78bc42a2b93afdf80c3d45cbc5264bb1d70d6b4 Mon Sep 17 00:00:00 2001 From: Sullivan SENECHAL Date: Tue, 21 Jun 2016 16:58:02 +0200 Subject: [PATCH 1/2] Introduce AbstractEnum::getClassPrefixedKeys The goal is to extract the class prefix logic from the form type to be used elsewhere. --- README.md | 10 +++++ src/AbstractEnum.php | 45 +++++++++++++++++++++-- src/Bridge/Symfony/Form/Type/EnumType.php | 12 ++---- test/AbstractEnumTest.php | 39 ++++++++++++++++++++ 4 files changed, 94 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 44ac540..dca04e2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Additionally, you may get all the constants in your class as a hash: ```php DaysOfWeek::getConstants(); DaysOfWeek::getConstants('strtolower'); // Will combine your values with `DaysOfWeek::getKeys($callback)`. +DaysOfWeek::getConstants('strtolower', true); // Values combine with `DaysOfWeek::getClassPrefixedKeys($callback)`. +DaysOfWeek::getConstants('strtolower', true, '.'); // Same with `DaysOfWeek::getClassPrefixedKeys($callback, $separator)`. ``` You may also get all the keys in your class as an array: @@ -58,6 +60,14 @@ DaysOfWeek::getKeys(); DaysOfWeek::getKeys('strtolower'); // Will call `array_map` with the given callback. ``` +Or the key with the enum class prefix: + +```php +DaysOfWeek::getClassPrefixedKeys(); +DaysOfWeek::getClassPrefixedKeys('strtolower'); // Will call `array_map` with the given callback. +DaysOfWeek::getClassPrefixedKeys('strtolower', '.'); // Replace the namespace separator ('_' by default). +``` + ### Advanced usage If you need to get the constants from a class you cannot modify, or from an diff --git a/src/AbstractEnum.php b/src/AbstractEnum.php index e1b02bd..4087280 100644 --- a/src/AbstractEnum.php +++ b/src/AbstractEnum.php @@ -2,12 +2,19 @@ namespace Greg0ire\Enum; +use Doctrine\Common\Inflector\Inflector; + /** * @author Grégoire Paris * @author Sullivan Senechal */ abstract class AbstractEnum { + /** + * @var string + */ + public static $defaultNamespaceSeparator = '_'; + private static $constCache = []; /** @@ -15,12 +22,15 @@ abstract class AbstractEnum * them in a local property for performance, before returning them. * * @param callable|null $keysCallback + * @param bool $classPrefixed True if you want the enum class prefix on each keys, false otherwise. + * @param string $namespaceSeparator Only relevant if $classPrefixed is set to true. * * @return array a hash with your constants and their value. Useful for * building a choice widget */ - final public static function getConstants($keysCallback = null) + final public static function getConstants($keysCallback = null, $classPrefixed = false, $namespaceSeparator = null) { + $namespaceSeparator = $namespaceSeparator ?: static::$defaultNamespaceSeparator; $enumTypes = static::getEnumTypes(); $enums = []; @@ -44,8 +54,13 @@ final public static function getConstants($keysCallback = null) } } - if (is_callable($keysCallback)) { - return array_combine(static::getKeys($keysCallback), $enums); + if (is_callable($keysCallback) || $classPrefixed) { + return array_combine( + $classPrefixed + ? static::getClassPrefixedKeys($keysCallback, $namespaceSeparator) + : static::getKeys($keysCallback), + $enums + ); } return $enums; @@ -69,6 +84,30 @@ final public static function getKeys($callback = null) return $keys; } + /** + * @param callable|null $callback A callable function compatible with array_map + * @param string|null $namespaceSeparator Choose which character should replace namespaces separation. + * Example: With Foo\BarMagic enum class with '.' separator, + * it will be converted to foo.bar_magic.YOUR_KEY + * + * @return string[] + */ + final public static function getClassPrefixedKeys($callback = null, $namespaceSeparator = null) + { + $namespaceSeparator = $namespaceSeparator ?: static::$defaultNamespaceSeparator; + $classKey = str_replace('\\', $namespaceSeparator, Inflector::tableize(static::class)); + + $keys = static::getKeys(function ($key) use ($namespaceSeparator, $classKey) { + return $classKey.$namespaceSeparator.$key; + }); + + if (is_callable($callback)) { + return array_map($callback, $keys); + } + + return $keys; + } + /** * Checks whether a constant with this name is defined. * diff --git a/src/Bridge/Symfony/Form/Type/EnumType.php b/src/Bridge/Symfony/Form/Type/EnumType.php index 16a66c6..4cd93b2 100644 --- a/src/Bridge/Symfony/Form/Type/EnumType.php +++ b/src/Bridge/Symfony/Form/Type/EnumType.php @@ -2,9 +2,7 @@ namespace Greg0ire\Enum\Bridge\Symfony\Form\Type; -use Doctrine\Common\Inflector\Inflector; use Greg0ire\Enum\AbstractEnum; -use Greg0ire\Enum\Bridge\Symfony\Validator\Constraint\Enum; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -37,13 +35,9 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefault('choices', function (Options $options) { $class = $options['class']; - $keys = call_user_func([$class, 'getKeys'], 'strtolower'); - if ($options['prefix_label_with_class']) { - array_walk($keys, function (&$key) use ($class) { - $classKey = str_replace('\\', '_', Inflector::tableize($class)); - $key = $classKey.'_'.$key; - }); - } + $keys = $options['prefix_label_with_class'] + ? call_user_func([$class, 'getClassPrefixedKeys'], 'strtolower') + : call_user_func([$class, 'getKeys'], 'strtolower'); $choices = array_combine($keys, call_user_func([$class, 'getConstants'])); // SF <3.1 BC diff --git a/test/AbstractEnumTest.php b/test/AbstractEnumTest.php index c4c5a55..0c8e432 100644 --- a/test/AbstractEnumTest.php +++ b/test/AbstractEnumTest.php @@ -38,6 +38,15 @@ public function testFooGetConstants() ], FooEnum::getConstants('strtolower') ); + + $this->assertSame( + [ + 'greg0ire.enum.tests.fixtures.foo_enum.god' => 'Dieu', + 'greg0ire.enum.tests.fixtures.foo_enum.chuck' => 'Chuck Norris', + 'greg0ire.enum.tests.fixtures.foo_enum.guitry' => 'Sacha Guitry', + ], + FooEnum::getConstants('strtolower', true, '.') + ); } public function testAllGetConstants() @@ -86,6 +95,36 @@ public function testFooGetKeys() ); } + public function testFooGetClassPrefixedKeys() + { + $this->assertSame( + [ + 'greg0ire_enum_tests_fixtures_foo_enum_GOD', + 'greg0ire_enum_tests_fixtures_foo_enum_CHUCK', + 'greg0ire_enum_tests_fixtures_foo_enum_GUITRY', + ], + FooEnum::getClassPrefixedKeys() + ); + + $this->assertSame( + [ + 'greg0ire_enum_tests_fixtures_foo_enum_god', + 'greg0ire_enum_tests_fixtures_foo_enum_chuck', + 'greg0ire_enum_tests_fixtures_foo_enum_guitry', + ], + FooEnum::getClassPrefixedKeys('strtolower') + ); + + $this->assertSame( + [ + 'greg0ire.enum.tests.fixtures.foo_enum.god', + 'greg0ire.enum.tests.fixtures.foo_enum.chuck', + 'greg0ire.enum.tests.fixtures.foo_enum.guitry', + ], + FooEnum::getClassPrefixedKeys('strtolower', '.') + ); + } + public function testsIsValidName() { $this->assertFalse(DummyEnum::isValidName('fiRsT')); From 34dffcada9fd2f01c6ed91f0bb690f22d5ff1ed7 Mon Sep 17 00:00:00 2001 From: Sullivan SENECHAL Date: Tue, 21 Jun 2016 17:56:33 +0200 Subject: [PATCH 2/2] Twig integration Add an optional Twig integration to have an extension for enum labels. --- README.md | 47 +++++++++++ composer.json | 6 +- .../Symfony/Bundle/Greg0ireEnumBundle.php | 20 +++++ .../Greg0ireEnumExtension.php | 33 ++++++++ src/Bridge/Symfony/Resources/config/twig.xml | 11 +++ src/Bridge/Twig/Extension/EnumExtension.php | 83 +++++++++++++++++++ .../Symfony/Bundle/Greg0ireEnumBundleTest.php | 38 +++++++++ .../Greg0ireEnumExtensionTest.php | 36 ++++++++ .../Twig/Extension/EnumExtensionTest.php | 74 +++++++++++++++++ 9 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 src/Bridge/Symfony/Bundle/Greg0ireEnumBundle.php create mode 100644 src/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtension.php create mode 100644 src/Bridge/Symfony/Resources/config/twig.xml create mode 100644 src/Bridge/Twig/Extension/EnumExtension.php create mode 100644 test/Bridge/Symfony/Bundle/Greg0ireEnumBundleTest.php create mode 100644 test/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtensionTest.php create mode 100644 test/Bridge/Twig/Extension/EnumExtensionTest.php diff --git a/README.md b/README.md index dca04e2..8fd1e8a 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,53 @@ $view = $this->factory->create(EnumType::class, null, array( ))->createView(); ``` +#### Twig extension + +This package comes with an `enum_label` filter, available thanks to the `EnumExtension` Twig class. +You have to require the `twig/twig` package to get it working. + +The filter will try to return the constant label corresponding to the given value. + +It will try to translate it if possible. To enable translation, require the `symfony/translation` component +and pass a `Symfony\Component\Translation\TranslationInterface` instance on the `EnumExtension` constructor. + +If translation is not available, you will have the default label with class prefixing. + +Usage: + +```twig +{{ value|enum_label('Your\\Enum\\Class') }} +{{ value|enum_label('Your\\Enum\\Class', 'another_domain') }} {# Change the translation domain #} +{{ value|enum_label('Your\\Enum\\Class', false) }} {# Disable translation. In this case the class prefix wont be added #} +{{ value|enum_label('Your\\Enum\\Class', false, true) }} {# Disable translation but keep class prefix #} +{{ value|enum_label('Your\\Enum\\Class', false, true, '.') }} {# Disable translation but keep class prefix with a custom separator #} +``` + +##### Twig extension as a service + +On Symfony projects, the extension can be autoloaded. +First, you have to require the `symfony/framework-bundle` and `symfony/twig-bundle` packages, or use Symfony fullstack. + +Then, register the bundle in the kernel of your application: + +``` php +// app/AppKernel.php + +public function registerBundles() +{ + $bundles = array( + // ... + new Greg0ire\Enum\Bridge\Symfony\Bundle\Greg0ireEnumBundle(), + ); + + // ... + + return $bundles +} +``` + +That's all. You can now directly use the filter. + ## Contributing see [CONTRIBUTING.md][1] diff --git a/composer.json b/composer.json index 1a3d327..8e7e09f 100644 --- a/composer.json +++ b/composer.json @@ -15,10 +15,14 @@ "doctrine/inflector": "^1.0" }, "require-dev": { + "matthiasnoback/symfony-dependency-injection-test": "^0.7.6", "phpunit/phpunit": "^4.1", "sllh/php-cs-fixer-styleci-bridge": "^2.0", "symfony/form": "^2.7 || ^3.0", - "symfony/validator": "^2.7 || ^3.0" + "symfony/framework-bundle": "^2.7 || ^3.0", + "symfony/twig-bundle": "^2.7 || ^3.0", + "symfony/validator": "^2.7 || ^3.0", + "twig/twig": "^1.24" }, "suggest": { "symfony/form": "To use enum form type", diff --git a/src/Bridge/Symfony/Bundle/Greg0ireEnumBundle.php b/src/Bridge/Symfony/Bundle/Greg0ireEnumBundle.php new file mode 100644 index 0000000..72ef788 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Greg0ireEnumBundle.php @@ -0,0 +1,20 @@ + + */ +final class Greg0ireEnumBundle extends Bundle +{ + /** + * {@inheritdoc} + */ + protected function getContainerExtensionClass() + { + return Greg0ireEnumExtension::class; + } +} diff --git a/src/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtension.php b/src/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtension.php new file mode 100644 index 0000000..74e0cfa --- /dev/null +++ b/src/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtension.php @@ -0,0 +1,33 @@ + + */ +final class Greg0ireEnumExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + + if (class_exists(\Twig_Extension::class)) { + $loader->load('twig.xml'); + + if (class_exists(Translator::class)) { + $container->getDefinition('greg0ire_enum.twig.extension.enum') + ->addArgument(new Reference('translator.default')); + } + } + } +} diff --git a/src/Bridge/Symfony/Resources/config/twig.xml b/src/Bridge/Symfony/Resources/config/twig.xml new file mode 100644 index 0000000..ba99985 --- /dev/null +++ b/src/Bridge/Symfony/Resources/config/twig.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/Bridge/Twig/Extension/EnumExtension.php b/src/Bridge/Twig/Extension/EnumExtension.php new file mode 100644 index 0000000..0f84c9f --- /dev/null +++ b/src/Bridge/Twig/Extension/EnumExtension.php @@ -0,0 +1,83 @@ + + */ +final class EnumExtension extends \Twig_Extension +{ + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @param TranslatorInterface $translator + */ + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return [ + new \Twig_SimpleFilter('enum_label', [$this, 'label']), + ]; + } + + /** + * Displays the label corresponding to a specific value of an enumeration. + * + * @param mixed $value Must exists in the enumeration class specified with $class + * @param string $class The enum class name + * @param string|bool $translationDomain The translation domain to use if the translator if available. + * string: Use the specified one + * null: Use the default one + * false: Do not use the translator + * @param bool $classPrefixed Prefix the label with the enum class. Defaults to true if the translator + * is available and enabled, false otherwise. + * @param string $namespaceSeparator Namespace separator to use with the class prefix. + * This takes effect only if $classPrefixed is true. + * + * @return string + */ + public function label($value, $class, $translationDomain = null, $classPrefixed = null, $namespaceSeparator = null) + { + // Determine if the translator can be used or not. + $useTranslation = $this->translator instanceof TranslatorInterface + && (is_null($translationDomain) || is_string($translationDomain)); + + // If not defined, guess the default behavior. + if (is_null($classPrefixed)) { + $classPrefixed = $useTranslation; + } + + $label = array_search( + $value, + call_user_func([$class, 'getConstants'], 'strtolower', $classPrefixed, $namespaceSeparator) + ); + + if ($useTranslation) { + $translatedLabel = $this->translator->trans($label, [], $translationDomain); + + return $translatedLabel ?: $label; + } + + return $label; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'greg0ire_enum'; + } +} diff --git a/test/Bridge/Symfony/Bundle/Greg0ireEnumBundleTest.php b/test/Bridge/Symfony/Bundle/Greg0ireEnumBundleTest.php new file mode 100644 index 0000000..054948d --- /dev/null +++ b/test/Bridge/Symfony/Bundle/Greg0ireEnumBundleTest.php @@ -0,0 +1,38 @@ + + */ +final class Greg0ireEnumBundleTest extends AbstractContainerBuilderTestCase +{ + /** + * @var Greg0ireEnumBundle + */ + private $bundle; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + parent::setUp(); + + $this->bundle = new Greg0ireEnumBundle(); + } + + public function testBuild() + { + $this->bundle->build($this->container); + } + + public function testGetContainerExtension() + { + $this->assertInstanceOf(Greg0ireEnumExtension::class, $this->bundle->getContainerExtension()); + } +} diff --git a/test/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtensionTest.php b/test/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtensionTest.php new file mode 100644 index 0000000..1f1ad2b --- /dev/null +++ b/test/Bridge/Symfony/DependencyInjection/Greg0ireEnumExtensionTest.php @@ -0,0 +1,36 @@ + + */ +final class Greg0ireEnumExtensionTest extends AbstractExtensionTestCase +{ + public function testLoad() + { + $this->load(); + + $this->assertContainerBuilderHasService('greg0ire_enum.twig.extension.enum', EnumExtension::class); + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + 'greg0ire_enum.twig.extension.enum', + 0, + new Reference('translator.default') + ); + } + + /** + * {@inheritdoc} + */ + protected function getContainerExtensions() + { + return [ + new Greg0ireEnumExtension(), + ]; + } +} diff --git a/test/Bridge/Twig/Extension/EnumExtensionTest.php b/test/Bridge/Twig/Extension/EnumExtensionTest.php new file mode 100644 index 0000000..3506583 --- /dev/null +++ b/test/Bridge/Twig/Extension/EnumExtensionTest.php @@ -0,0 +1,74 @@ + + */ +final class EnumExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var TranslatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $translator; + + /** + * @var EnumExtension + */ + private $extension; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->translator = $this->getMock(TranslatorInterface::class); + $this->extension = new EnumExtension($this->translator); + } + + public function testEnvironment() + { + $twig = new \Twig_Environment(); + $twig->addExtension($this->extension); + + $this->assertTrue($twig->hasExtension('greg0ire_enum')); + $this->assertInstanceOf(\Twig_SimpleFilter::class, $twig->getFilter('enum_label')); + } + + /** + * @dataProvider getLabels + */ + public function testLabel($value, $class, $classPrefix, $separator, $expectedResult) + { + $this->assertSame( + $expectedResult, + $this->extension->label($value, $class, false, $classPrefix, $separator) + ); + } + + public function getLabels() + { + return [ + [FooInterface::CHUCK, FooEnum::class, false, null, 'chuck'], + [FooInterface::CHUCK, FooEnum::class, true, null, 'greg0ire_enum_tests_fixtures_foo_enum_chuck'], + [FooInterface::CHUCK, FooEnum::class, true, '.', 'greg0ire.enum.tests.fixtures.foo_enum.chuck'], + ]; + } + + public function testLabelWithTranslator() + { + $this->translator->expects($this->once()) + ->method('trans')->with('greg0ire_enum_tests_fixtures_foo_enum_chuck', [], 'test'); + + $this->assertSame( + 'greg0ire_enum_tests_fixtures_foo_enum_chuck', + $this->extension->label(FooInterface::CHUCK, FooEnum::class, 'test'), + 'Without any available translation, the filter should just return the key.' + ); + } +}