Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use guessing with DTO instead of Entity? #226

Open
craigh opened this issue Nov 17, 2022 · 6 comments
Open

How to use guessing with DTO instead of Entity? #226

craigh opened this issue Nov 17, 2022 · 6 comments
Assignees
Labels

Comments

@craigh
Copy link

craigh commented Nov 17, 2022

I'm trying to use the FormType guessing feature of this bundle, but with a DTO and not an Entity. Is this possible? I see the guessing uses the Type - which I assume is from the @ORM\Column(type="MyCoolEnum") declaration.

$fieldType = $metadata->getTypeOfField($property);

I guess Symfony uses the validation constraints instead. Is there a way this could be done?

@fre5h
Copy link
Owner

fre5h commented Nov 17, 2022

@craigh do you get some error? if yes, then please write it here and also show me your form type.

@fre5h fre5h self-assigned this Nov 17, 2022
@fre5h fre5h added the question label Nov 17, 2022
@craigh
Copy link
Author

craigh commented Nov 17, 2022

Hello!

No, I do not receive any error, the guessing just fails so the input is displayed as a simple text box.

The form generation is dynamic and complicated, but in the end it is simply this:

$builder->add($field->name, $field->type, $options);

where:

$field->name = 'state'
$field->type = null
$options = []

As I said, the form data is a DTO instead of an Entity. So I have tried various versions of attributes in the DTO definition:

    #[DoctrineAssert\EnumType(USStateType::class)]
    public string $state;
    #[DoctrineAssert\EnumType(USStateType::class)]
    public USStateType $state;
    public USStateType $state;

none seem to have any effect.

@craigh
Copy link
Author

craigh commented Nov 17, 2022

I am using the same Enum in a 'regular' form and it works fine there. So the Enum is setup properly when using an Entity-backed form.

Thank you for quickly responding and trying to help me! 🙏

@fre5h
Copy link
Owner

fre5h commented Nov 17, 2022

It's not possible to use form guesser for DTOs now. Because

class EnumTypeGuesser extends DoctrineOrmTypeGuesser

Enum Guesser extends Doctrine ORM Guesser, so it expects the mapped entity. To do what you want is needed a new Guesser, which will does somehow detection from any Plain PHP Class, not only from entities.

@craigh
Copy link
Author

craigh commented Nov 17, 2022

Thank you, this was my assumption as well. I was hoping you might be able to extend Symfony's Validation guesser to add Enum types as well. I guess this goes in as a feature request.

thanks!

@craigh
Copy link
Author

craigh commented Nov 18, 2022

Here is something I hacked together. You could clean it up and add it to the bundle.

// src/Form/Extension/Validator/EnumValidatorTypeGuesser.php
<?php

namespace App\Form\Extension\Validator;

use Fresh\DoctrineEnumBundle\DBAL\Types\AbstractEnumType;
use Fresh\DoctrineEnumBundle\Exception\EnumType\EnumTypeIsRegisteredButClassDoesNotExistException;
use Fresh\DoctrineEnumBundle\Validator\Constraints\EnumType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;

class EnumValidatorTypeGuesser implements FormTypeGuesserInterface
{
    /** @var string[] */
    private array $registeredEnumTypes = [];

    public function __construct(
        private readonly MetadataFactoryInterface $metadataFactory,
        array $registeredTypes,
    ) {
        foreach ($registeredTypes as $type => $details) {
            $this->registeredEnumTypes[$type] = $details['class'];
        }
    }

    public function guessType(string $class, string $property): ?TypeGuess
    {
        return $this->guess($class, $property, function (Constraint $constraint) {
            return $this->guessTypeForConstraint($constraint);
        });
    }

    public function guessRequired(string $class, string $property): ?ValueGuess
    {
        return null;
    }

    public function guessMaxLength(string $class, string $property): ?ValueGuess
    {
        return null;
    }

    public function guessPattern(string $class, string $property): ?ValueGuess
    {
        return null;
    }

    /**
     * Guesses a field class name for a given constraint.
     */
    public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess
    {
        if ($constraint instanceof EnumType) {
            if (!\in_array($constraint->entity, $this->registeredEnumTypes, true)) {
                return null;
            }
            $registeredEnumTypeFQCN = $constraint->entity;

            if (!\class_exists($constraint->entity)) {
                $exceptionMessage = \sprintf(
                    'ENUM type "%s" is registered as "%s", but that class does not exist',
                    $constraint->entity,
                    $registeredEnumTypeFQCN
                );

                throw new EnumTypeIsRegisteredButClassDoesNotExistException($exceptionMessage);
            }

            if (!\is_subclass_of($registeredEnumTypeFQCN, AbstractEnumType::class)) {
                return null;
            }

            /** @var AbstractEnumType<int|string, int|string> $registeredEnumTypeFQCN */
            $parameters = [
                'choices' => $registeredEnumTypeFQCN::getChoices(), // Get the choices from the fully qualified class name
            ];

            return new TypeGuess(ChoiceType::class, $parameters, Guess::VERY_HIGH_CONFIDENCE);
        }

        return null;
    }

    /**
     * Iterates over the constraints of a property, executes a constraints on
     * them and returns the best guess.
     */
    protected function guess(string $class, string $property, \Closure $closure, $defaultValue = null): Guess|null
    {
        $guesses = [];
        $classMetadata = $this->metadataFactory->getMetadataFor($class);

        if ($classMetadata instanceof ClassMetadataInterface && $classMetadata->hasPropertyMetadata($property)) {
            foreach ($classMetadata->getPropertyMetadata($property) as $memberMetadata) {
                foreach ($memberMetadata->getConstraints() as $constraint) {
                    if ($guess = $closure($constraint)) {
                        $guesses[] = $guess;
                    }
                }
            }
        }

        if ($defaultValue !== null) {
            $guesses[] = new ValueGuess($defaultValue, Guess::LOW_CONFIDENCE);
        }

        return Guess::getBestGuess($guesses);
    }
}

Config:

# services.yaml
    App\Form\Extension\Validator\EnumValidatorTypeGuesser:
        arguments:
            - '@validator.mapping.class_metadata_factory'
            - "%doctrine.dbal.connection_factory.types%"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants