FriendsOfSymfony / FOSRestBundle

This Bundle provides various tools to rapidly develop RESTful API's with Symfony
http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
MIT License
2.79k stars 704 forks source link

EntityToIdObjectTransformer not working Symfony 4.4.x #2274

Closed astrowild99 closed 3 years ago

astrowild99 commented 3 years ago

Following the official documentation page on "Step 2: The view layer" when trying to create the transformer quoted in the docs under "Data Transformation" It doesn't work. After some simple debugging it looks like the problem is on new validations of the TextType form type that prevents the data to be passed to the EntityToIdObjectTransformer::reverseTransform is not called at all. Thank you for your constant work in improving Symfony. Hope this could help.

xabbuh commented 3 years ago

Can you create a small example application that allows to reproduce your issue?

astrowild99 commented 3 years ago

sure: here is the problem i have: I try to create a controller to answer to this request: { "username": "uname", "password": "pwd", "email": "uname@mail.me", "role": { "id": 1 } } and i have two entities:

 * @ORM\Entity
 * @ORM\Table(name="role")
 */
class Role
{
    const LEVEL_ADMIN = 0;
    /* voluntarily left empty for some new level */
    const LEVEL_DOCTOR = 2;
    const LEVEL_USER = 3;

    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=20, nullable=false)
     */
    private $name;

    /**
     * @ORM\Column(type="integer", nullable=false)
     */
    private $level;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\User", mappedBy="role")
     */
    private $users;

    public function __construct()
    {
        $this->users = new ArrayCollection();
    }
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 * @UniqueEntity("username")
 * @UniqueEntity("email")
 */
class User extends BaseUser
{
    /**
     * I am forced to use the normal id to let the oauth work
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @Serializer\Groups({"Default"})
     */
    protected $id;

    /**
     * @Serializer\Groups({"Default"})
     * @Assert\NotBlank
     */
    protected $username;

    /**
     * @Serializer\Groups({""})
     */
    protected $usernameCanonical;

    /**
     * @Serializer\Groups({"Default"})
     * @Assert\NotBlank
     * @Assert\Email
     */
    protected $email;
// [...]
/**
     * @ORM\ManyToOne(targetEntity="App\Entity\Role", inversedBy="users")
     * @ORM\JoinColumn(name="id_role", referencedColumnName="id")
     * @Serializer\Groups({"Default"})
     */
    private $role;

now i try to register a new user via the form:

{
    /**
     * @var EntityManagerInterface
     */
    private $em;

    /**
     * UserCredentialsType constructor.
     * @param EntityManagerInterface $em
     */
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    /**
     * When i edit the user i wanna set the new Password only if required
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $roleTransformer = new EntityToIdObjectTransformer($this->em, Role::class);
        $builder
            ->add('username', TextType::class, array(
                'required'  => true,
                'mapped'    => true,
                'documentation' => array(
                    'type'  => 'string',
                    'description'   => 'new user\'s username'
                )
            ))
            ->add('password', TextType::class, array(
                'required'  => true,
                'mapped'    => false,
                'documentation' => array(
                    'type'  => 'string',
                    'description'   => 'the user\'s password, in plain text'
                )
            ))
            ->add('email', EmailType::class, array(
                'required'  => true,
                'mapped'    => true,
                'documentation' => array(
                    'type'  => 'string',
                    'description'   => 'the user\'s email'
                )
            ))
            ->add($builder->create('role', TextType::class)->addModelTransformer($roleTransformer))
            ->add('submit', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'        => User::class,
            'csrf_protection'   => false,
            'allow_extra_fields'    => true
        ));
    }
}

and call via the following controller

class RegistrationController extends AbstractFOSRestController
{
    /**
     * Register as a new user, this call does NOT need authentication
     * @Rest\Post("/register")
     * @SWG\Response(
     *     response="201",
     *     description="Returns the newly created user",
     *     @SWG\Schema(type="object", ref=@Model(type=User::class, groups={"Default"})))
     * @SWG\Response(
     *     response="400",
     *     description="bad request",
     *     @SWG\Schema(type="object", ref=@Model(type=FormError::class, groups={"Default"})))
     * @SWG\Parameter(
     *     name="form",
     *     in="body",
     *     @Model(type=UserCredentialsType::class))
     *
     * @param Request $request
     * @param AuthenticationHelper $authenticationHelper
     * @return View
     */
    public function postRegistrationAction(Request $request, AuthenticationHelper $authenticationHelper)
    {
        $user = new User();
        $form = $this->get('form.factory')->createNamed('', UserCredentialsType::class, $user);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $user = $authenticationHelper->createNewUserCredentials($user, $form['password']->getData());
            $em->persist($user);
            $em->flush();
            return View::create($user, Response::HTTP_CREATED);
        }
        return View::create($form->getErrors(), Response::HTTP_BAD_REQUEST);
    }
}

I tried to follow the documentation in every step but the controller returns code 400 and debugging the form creation, as i wrote before, the field 'role' is empty and so the Transformer is not called either, Thank you For your help

astrowild99 commented 3 years ago

Sorry for this request, i found out the issue: for whoever having my same issue check the decoders in

fos_rest:
 body_listener:
  decoders:
   json: fos_rest.decoder.jsontoform

Hope this is helpful for others.