Our app has members, for which we can have :
- a status which must be one of :
new,activeordisabled. - a list of subscribed mailing lists which must be any of :
newsletterorcommercial.
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 informationFor 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 %}