Un bundle OpenSource qui apporte un système d'énumération totalement intégré à votre projet Symfony.
Lorsque l'on créé une application il arrive fréquemment que nous cherchions à contraindre les valeurs possibles d'une propriété.
Des propriétés pour lesquelles on souhaite ajouter une couche de validation et/ou pouvoir afficher à l'utilisateur un <select>
(multiple ou non).
Symfony ne propose pas de système tout prêt pour gérer ce cas d'usages (en tout cas, rien qui ne soit suffisamment simple à l'usage).
On se retrouve donc souvent à dupliquer beaucoup de code, à grand coup de @Assert\Choice()
et de $builder->add('property', ChoiceType::class, ['choices' => []])
.
Ce qui est assez dommage.
De ce constat, est né un bundle, qui se propose de packager les fonctionnalités du framework autour d'une nouvelle notion : les énumérations.
Le contrat de création de ce bundle était le suivant :
Vous pouvez sans attendre retrouver ce bundle sur GitHub et Packagist .
Ajoutez le bundle en dépendance de votre projet.
composer require yokai/enum-bundle
Si vous n'êtes pas encore passés sous symfony/flex
(vous devriez y songer), ajoutez le bundle à votre kernel.
new Yokai\EnumBundle\YokaiEnumBundle(),
Voilà, c'est installé, reste à l'utiliser.
Disons que votre application a des utilisateurs. Ces utilisateurs ont (entre autres) un statut et peuvent souscrire à des mailing lists.
Note : On ne parle pas de persistance ici (pas de mapping Doctrine par exemple), car cela n'influe pas sur le bundle.
<?php
namespace App\Model;
use Symfony\Component\Validator\Constraints as Assert;
class User
{
public const STATUS_NEW = 'new';
public const STATUS_ACTIVE = 'active';
public const STATUS_DISABLED = 'disabled';
public const SUBSCRIBE_NEWSLETTER = 'newsletter';
public const SUBSCRIBE_COMMERCIAL = 'commercial';
/**
* @Assert\NotNull()
* @Assert\Choice({User::STATUS_NEW, User::STATUS_ACTIVE, User::STATUS_DISABLED})
*/
public string $status = self::STATUS_NEW;
/**
* @Assert\Choice({User::SUBSCRIBE_NEWSLETTER, User::SUBSCRIBE_COMMERCIAL}, multiple=true)
*/
public array $subscriptions;
}
Les propriétés $status
& $subscriptions
ne sont pas des propriétés libres, la liste des valeurs possibles est restreinte.
On utilise la contrainte Choice pour faire appliquer la validation.
Les valeurs possibles pour ces propriétés sont fixées dans des constantes de classe, car c'est pratique si l'on souhaite faire des règles de gestion sur ces valeurs par la suite.
Si l'on souhaite proposer un formulaire permettant d'éditer ces propriétés, on s'en sort avec le ChoiceType :
<?php
namespace App\Form\Type;
use App\Model\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(
'status',
ChoiceType::class,
[
'required' => true,
'choices' => [
'user‧status‧new' => User::STATUS_NEW,
'user‧status‧active' => User::STATUS_ACTIVE,
'user‧status‧disabled' => User::STATUS_DISABLED,
],
]
)
->add(
'subscriptions',
ChoiceType::class,
[
'required' => false,
'multiple' => true,
'choices' => [
'user‧subscription‧newsletter' => User::SUBSCRIBE_NEWSLETTER,
'user‧subscription.commercial' => User::SUBSCRIBE_COMMERCIAL,
],
]
)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class', User::class);
}
}
Enfin, si on souhaite afficher les valeurs d'un utilisateur existant dans un template Twig :
<label>Status</label>
<p>{{ ('user‧status.'~user‧status)|trans }}</p>
<label>Subscriptions</label>
{% for subscription in user‧subscriptions %}
<p>{{ ('user‧subscription.'~subscription)|trans }}</p>
{% endfor %}
Le problème principal, c'est la duplication du code. Evidemment, si vous n'avez qu'une seule énumération ce n'est pas très grave, mais quand votre application grossira, vous allez perdre du temps en maintenance et en évolutions.
Mais même avec une seule énumération, nous avons aussi un problème de fragmentation :
Nous allons déclarer notre énumération, en enregistrant un service
services:
enum‧user‧status:
class: Yokai\EnumBundle\ConstantListTranslatedEnum
arguments:
$constantsPattern: 'App\Model\User::STATUS_*'
$transPattern: 'user‧status.%s'
$name: 'user‧status'
enum‧user‧subscription:
class: Yokai\EnumBundle\ConstantListTranslatedEnum
arguments:
$constantsPattern: 'App\Model\User::SUBSCRIPTION_*'
$transPattern: 'user‧subscription.%s'
$name: 'user‧subscription'
Le bundle va ajouter automatiquement le tag yokai_enum‧enum
sur ces 2 services, car ils implémentent l'interface Yokai\EnumBundle\EnumInterface
, et enregistrer ces services auprès du "registre des énumérations" (Yokai\EnumBundle\EnumRegistry
).
note il existe plusieurs façons de déclarer une énumération. Une liste de constantes définies en configuration n'est qu'une des façons possibles.
Maintenant, nous allons utiliser ces énumérations et modifier notre code.
Dans le modèle, nous allons remplacer la contrainte Choice
de Symfony par Enum
, fournie par le bundle. Et pour chacune de ces annotations on va préciser le name
de l'enum dont nous avons besoin.
<?php
namespace App\Model;
use Symfony\Component\Validator\Constraints as Assert;
+use Yokai\EnumBundle\Validator\Constraints\Enum;
class User
{
public const STATUS_NEW = 'new';
public const STATUS_ACTIVE = 'active';
public const STATUS_DISABLED = 'disabled';
public const SUBSCRIBE_NEWSLETTER = 'newsletter';
public const SUBSCRIBE_COMMERCIAL = 'commercial';
/**
* @Assert\NotNull()
- * @Assert\Choice({User::STATUS_NEW, User::STATUS_ACTIVE, User::STATUS_DISABLED})
+ * @Enum("user.status")
*/
public string $status = self::STATUS_NEW;
/**
- * @Assert\Choice({User::SUBSCRIBE_NEWSLETTER, User::SUBSCRIBE_COMMERCIAL}, multiple=true)
+ * @Enum("user.subscription", multiple=true)
*/
public array $subscriptions;
}
Dans le formulaire, nous allons tout retirer excepté l'ajout des champs. Le bundle se chargera pour nous de trouver le bon FormType
à affecter à notre champ.
<?php
namespace App\Form\Type;
use App\Model\User;
use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
- ->add(
- 'status',
- ChoiceType::class,
- [
- 'required' => true,
- 'choices' => [
- 'user‧status‧new' => User::STATUS_NEW,
- 'user‧status‧active' => User::STATUS_ACTIVE,
- 'user‧status‧disabled' => User::STATUS_DISABLED,
- ],
- ]
- )
+ ->add('status')
- ->add(
- 'subscriptions',
- ChoiceType::class,
- [
- 'required' => false,
- 'multiple' => true,
- 'choices' => [
- 'user‧subscription‧newsletter' => User::SUBSCRIBE_NEWSLETTER,
- 'user‧subscription.commercial' => User::SUBSCRIBE_COMMERCIAL,
- ],
- ]
- )
+ ->add('subscriptions')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class', User::class);
}
}
Dans le template, on va remplacer la génération dynamique de la clé de traduction par un filtre que propose le bundle.
<label>Status</label>
-<p>{{ ('user‧status.'~user‧status)|trans }}</p>
+<p>{{ user‧status|enum_label('user‧status') }}</p>
<label>Subscriptions</label>
{% for subscription in user‧subscriptions %}
- <p>{{ ('user‧subscription.'~subscription)|trans }}</p>
+ <p>{{ subscription|enum_label('user‧subscription') }}</p>
{% endfor %}
Voilà ! A partir de maintenant, les valeurs possibles et les labels associés sont centralisés dans un seul et même concept !
En vérité, rien de plus que ce qui est décrit en préambule de l'article. Le bundle ré-utilise ce qui existe déjà dans Symfony. Il propose :
FormType
qui est une extension de ChoiceType
Constraint
qui est une extension de Choice
Validator
qui est une extension de ChoiceValidator
TypeGuesser
qui s'inspire grandement du ValidatorTypeGuesser
TwigExtension
sommaire pour afficher les labelsLe tout est orchestré par une seule et unique classe : EnumRegistry
auprès de qui sont enregistrés toutes les énumérations de votre application.
Dans sa version actuelle (3.x
), les énumérations ne peuvent avoir que des valeurs de type string
.
Avec l'arrivée de PHP 8.1 et de son support natif des énumérations, l'écosystème va naturellement s'orienter vers des énumérations dont les valeurs sont des objets.
C'est pourquoi la prochaine version majeure du bundle (4.x
) donnera la possibilité de définir des énumérations avec des valeurs très diverses : string
, int
, bool
, ... et les énumérations natives !
Cette version permettra également une intégration avec la, très populaire, librairie myclabs/php-enum
.