Our app has members, for which we can have :
- a status which must be one of :
new
,active
ordisabled
. - a list of subscribed mailing lists which must be any of :
newsletter
orcommercial
.
Our model has the properties to hold these values. These properties are validated using using Symfony's Choice Constraint.
Possible values for these properties are stored in class constant. This is a good thing to keep these values as close as possible from the model.
<?php
namespace App\Model;
use Symfony\Component\Validator\Constraints as Assert;
class Member
{
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(choices: [Member::STATUS_NEW, Member::STATUS_ACTIVE, Member::STATUS_DISABLED])]
public string $status = self::STATUS_NEW;
#[Assert\Choice(choices: [Member::SUBSCRIBE_NEWSLETTER, Member::SUBSCRIBE_COMMERCIAL], multiple: true)]
public array $subscriptions = [];
}
In the messages
translation catalog, we declared our labels:
# translations/messages.en.yaml
member:
status:
new: New
active: Active
disabled: Disabled
subscription:
newsletter: Our weekly newsletter
commercial: Our montly commercial information
For the form associated with that model, we used a ChoiceType
<?php
namespace App\Form\Type;
use App\Model\Member;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MemberType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(
'status',
ChoiceType::class,
[
'required' => true,
'choices' => [
'member.status.new' => Member::STATUS_NEW,
'member.status.active' => Member::STATUS_ACTIVE,
'member.status.disabled' => Member::STATUS_DISABLED,
],
]
)
->add(
'subscriptions',
ChoiceType::class,
[
'required' => false,
'multiple' => true,
'choices' => [
'member.subscription.newsletter' => Member::SUBSCRIBE_NEWSLETTER,
'member.subscription.commercial' => Member::SUBSCRIBE_COMMERCIAL,
],
]
)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class', Member::class);
}
}
Finally, in a Twig template, we end up with translated concatenation for our labels:
<label>Status</label>
<p>{{ ('member.status.'~member.status)|trans }}</p>
<label>Subscriptions</label>
{% for subscription in member.subscriptions %}
<p>{{ ('member.subscription.'~subscription)|trans }}</p>
{% endfor %}
After the bundle is installed, we will start the migration.
This is the first thing to do. Enums will centralize both possible values and labels for each value.
As the values already exists in our model,
we will use the ConstantListTranslatedEnum
base class to extract constants values as an enum.
<?php
namespace App\Enum;
use App\Model\Member;
use Symfony\Contracts\Translation\TranslatorInterface;
use Yokai\EnumBundle\ConstantListTranslatedEnum;
class MemberStatusEnum extends ConstantListTranslatedEnum
{
public function __construct(TranslatorInterface $translator)
{
parent::__construct(Member::class . '::STATUS_*', $translator, 'member.status.%s');
}
}
<?php
namespace App\Enum;
use App\Model\Member;
use Symfony\Contracts\Translation\TranslatorInterface;
use Yokai\EnumBundle\ConstantListTranslatedEnum;
class MemberSubscriptionEnum extends ConstantListTranslatedEnum
{
public function __construct(TranslatorInterface $translator)
{
parent::__construct(Member::class . '::SUBSCRIBE_*', $translator, 'member.subscription.%s');
}
}
Now that both enum exists, we will replace the Choice
constraint of our model properties with an Enum
constraint.
This is in that constraint that our enum name will be written.
<?php
namespace App\Model;
+use App\Enum\MemberStatusEnum;
+use App\Enum\MemberSubscriptionEnum;
use Symfony\Component\Validator\Constraints as Assert;
+use Yokai\EnumBundle\Validator\Constraints\Enum;
class Member
{
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(choices: [Member::STATUS_NEW, Member::STATUS_ACTIVE, Member::STATUS_DISABLED])]
+ #[Enum(enum: MemberStatusEnum::class)]
public string $status = self::STATUS_NEW;
- #[Assert\Choice(choices: [Member::SUBSCRIBE_NEWSLETTER, Member::SUBSCRIBE_COMMERCIAL], multiple: true)]
+ #[Enum(enum: MemberSubscriptionEnum::class, multiple: true)]
public array $subscriptions = [];
}
As we associated the Enum
constraint to our model properties, our form will now become very simple.
As far as you do not override the field type, the bundle will guess the appropriate form and options for you.
<?php
namespace App\Form\Type;
use App\Model\Member;
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 MemberType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
- ->add(
- 'status',
- ChoiceType::class,
- [
- 'required' => true,
- 'choices' => [
- 'member.status.new' => Member::STATUS_NEW,
- 'member.status.active' => Member::STATUS_ACTIVE,
- 'member.status.disabled' => Member::STATUS_DISABLED,
- ],
- ]
- )
+ ->add('status')
- ->add(
- 'subscriptions',
- ChoiceType::class,
- [
- 'required' => false,
- 'multiple' => true,
- 'choices' => [
- 'member.subscription.newsletter' => Member::SUBSCRIBE_NEWSLETTER,
- 'member.subscription.commercial' => Member::SUBSCRIBE_COMMERCIAL,
- ],
- ]
- )
+ ->add('subscriptions')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class', Member::class);
}
}
Finally, the template can be switched to use the enum_label
filter.
That label can be applied to any enum value. There is one only thing you need to provide : the enum name you want to use.
<label>Status</label>
-<p>{{ ('member.status.'~member.status)|trans }}</p>
+<p>{{ member.status|enum_label('App\\Enum\\MemberStatusEnum') }}</p>
<label>Subscriptions</label>
{% for subscription in member.subscriptions %}
- <p>{{ ('member.subscription.'~subscription)|trans }}</p>
+ <p>{{ subscription|enum_label('App\\Enum\\MemberSubscriptionEnum') }}</p>
{% endfor %}