Home > Enterprise >  Doctrine collection of enums and Symfony Form
Doctrine collection of enums and Symfony Form

Time:10-21

The user can choose which e-mail notifications he wants to receive.

Notification types are defined by enum (MyCLabs library):

use MyCLabs\Enum\Enum;

final class NotificationType extends Enum
{
    public const DEADLOCKED = 1;
    public const REJECTED = 2;
    public const SENT = 3;
    public const ACCEPTED = 4;
    public const REFUSED = 5;

    public function translationPath(): string
    {
        return 'user.notifications.'.$this->getKey();
    }
}

User has more notification types:

/**
 * @ORM\Entity()
 */
class Notification
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="notifications")
     */
    protected User $user;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    protected int $notificationType;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function getNotificationType(): NotificationType
    {
        return new NotificationType($this->notificationType);
    }

    public function setNotificationType(NotificationType $notificationType): self
    {
        $this->notificationType = $notificationType->getValue();

        return $this;
    }
}

User entity:

/**
 * @ORM\Entity()
 */
class User implements UserInterface, EquatableInterface
{
    /**
     * @var Collection|Notification[]
     * @ORM\OneToMany(targetEntity=Notification::class, mappedBy="user", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    protected Collection $notifications;
    
    //...
}

Is this the right solution? Now I have a problem to make Symfony Form for list of checkboxes.

Something like this (I know, it's wrong):

$builder->add('notifications', ChoiceType::class, [
    'choices' => NotificationType::values(),
    'expanded' => true,
    'multiple' => true,
    'choice_value' => 'value',
    'choice_label' => static function (NotificationType $type): string {
        return $type->translationPath();
    },
]);

Can I use build-in Symfony Form in my case? Or do you have better solution for relation "Entity manyToMany Enum".

CodePudding user response:

The problem is the 'notifications' form field is mapped with entity notifications field which is a collection of notifications, and you can't map a choicetype (which expects a single notification) with a collection of notifications

you need then use a CollectionType, which will make a field for every notification (in you case the field is the ChoiceType)

read more about the CollectionType

$builder->add('notifications', CollectionType::class, [
    'entry_type'   => ChoiceType::class,
    'entry_options'  => [
        'choices' => NotificationType::values(),
        'expanded' => true,
        'multiple' => true,
        'choice_value' => 'value',
        'choice_label' => static function (NotificationType $type): string {
            return 'notifications.types.'.$type->getKey();
        },
    ],
]);

CodePudding user response:

I wrote my EnumChoiceType with data transformer which transform Array of NotificationType to Collection of Notification entites and reverse versa.

Don't forgot set orphanRemoval=true on collection in entity.

Usage:

$builder->add('notifications', EnumChoiceType::class, [
    'enum' => NotificationType::class,
    'class' => Notification::class,
    'property' => 'notificationType',
    'object' => $options['data'],
    'expanded' => true,
    'multiple' => true,
]);

EnumChoiceType:

class EnumChoiceType extends AbstractType implements DataTransformerInterface
{
    /**
     * @var mixed
     */
    private $object;
    private string $property;
    private string $class;

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $this->object = $options['object'];
        $this->property = $options['property'];
        $this->class = $options['class'];

        if ($options['multiple']) {
            $builder->addModelTransformer($this);
        }
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setRequired([
            'enum',
        ]);

        $resolver->setDefaults([
            'choices' => static function (Options $options) {
                return call_user_func([$options['enum'], 'values']);
            },
            'choice_value' => 'value',
            'choice_label' => static function (Enum $enum): string {
                return $enum->translationPath();
            },
            'property' => 'enum',
            'object' => null,
            'class' => null,
        ]);
    }

    /**
     * @param mixed $value
     * @return Enum[]
     */
    public function transform($value): array
    {
        $array = [];

        foreach ($value as $item) {
            $array[] = PropertyAccess::createPropertyAccessor()->getValue($item, $this->property);
        }

        return $array;
    }

    /**
     * @param Enum[] $value
     */
    public function reverseTransform($value): ArrayCollection
    {
        $collection = new ArrayCollection();

        foreach ($value as $type) {
            $notification = new $this->class($this->object);
            PropertyAccess::createPropertyAccessor()->setValue($notification, $this->property, $type);

            $collection->add($notification);
        }

        return $collection;
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }
}
  • Related