3
votes

I've searched and not found anyone with this problem.

I've created my own Data Transformer as set out in the Cookbook and it all seems right but i get the error:

The form's view data is expected to be an instance of class Niche\SecurityBundle\Entity\BusinessUser, but is a(n) integer. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) integer to an instance of Niche\SecurityBundle\Entity\BusinessUser.

The transformer is below:

<?php
namespace Niche\SecurityBundle\Form\DataTransformer;

use JMS\SecurityExtraBundle\Security\Util\String;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Niche\SecurityBundle\Entity\BusinessUser;

class BusinessUserToIdTransformer implements DataTransformerInterface
{
/**
 * @var ObjectManager
 */
private $om;

/**
 * @param ObjectManager $om
 */
public function __construct(ObjectManager $om)
{
    $this->om = $om;
}

/**
 * Transforms an object (BusinessUser) to a string (number)
 * 
 * @param BusinessUser|null $businessUser
 * @return String
 */
public function transform($businessUser)
{
    if (null === $businessUser) {
        return "";
    }

    return $businessUser->getId();
}

/**
 * Transforms a string (number) to an object (BusinessUser).
 * 
 * @param string $number
 * 
 * @return BusinessUser|null
 * 
 * @throws TransformationFailedException if object (BusinessUser) is not found.
 */
public function reverseTransform($id)
{
    if (!$id) {
        return null;
    }

    $businessUser = $this->om
        ->getRepository('NicheSecurityBundle:BusinessUser')
        ->findOneById($id);

    if (null === $businessUser) {
        throw new TransformationFailedException(sprintf(
                'An issue with number "%s" does not exist!',
                $number
                ));

    }
            return $businessUser
}
}

and my form code is

<?php

namespace Niche\JobBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Niche\SecurityBundle\Form\DataTransformer\BusinessUserToIdTransformer;

class JobType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{

    $entityManager = $options['em'];
    $transformer = new BusinessUserToIdTransformer($entityManager);

    //Get the BusinessUser object passed in.
    $businessUser = $options['businessUser'];


    $builder->add('title', 'text');
    $builder->add('jobDescription', 'textarea', array(
            "label" => "Job Description", )
        );
    $builder->add('companyDescription', 'textarea', array(
            "label" => "Company Description", )
        );
    $builder->add('salary', 'text');
    $builder->add('category', 'entity', array(
        'class' => 'NicheJobBundle:Category',
        'property' => 'name',   
    ));
    $builder->add('responsibilities', 'textarea');
    $builder->add('closingDate', 'datetime');
    //Add Business User to the form - Need a way for a job to be added by site admin in addition, could just be site admin users logged in with a Business user account
    $builder->add(
            $builder->create('company', 'hidden', array(
                'data' => $businessUser,

            ))->addViewTransformer($transformer)

    );

}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Niche\JobBundle\Entity\Job'
    ));

    $resolver->setRequired(array(
            'em', 'businessUser'
    ));

    $resolver->setAllowedTypes(array(
        'em' => 'Doctrine\Common\Persistence\ObjectManager',
    ));
}

public function getName()
{
    return 'niche_jobbundle_jobtype';
}
}

The error message is confusing me as it appears it has converted the BusinessUser class to an integer. I also tried by putting in the data_class => null and the form then loaded but on submission I got an error that the field was empty though the ID in the hidden field appeared correctly hen the form was generated.

Can someone point me in the right direction as have a feeling its something very simple that im not seeing.

Thanks

UPDATE: I made some changes as realised I should be setting the company in the controller and passing it into the form, and that all seems to work but when the form is submitted I still get null in the database or if i set the field to @Assert\NotBlank() the form wont submit as field cannot be blank even though when I check the source I can see the Id of the BusinessUser in the hidden field.

*UPDATE - While implemeting Coma's suggestion below I finally realised where I had gone wrong - it was the transformer it was not returning the object * - If anyone else comes onto this question I would recommend Coma's solution below is much better than having to create a hidden each time. If it is a one off the solution above has been updated to return the object and should work fine.

2

2 Answers

7
votes

This is the way I handle entities with hidden inputs:

DataTransformer

<?php

namespace Comakai\CQZBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;

class EntityToIdTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;
    private $entityClass;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om, $entityClass)
    {
        $this->om = $om;
        $this->entityClass = $entityClass;
    }

    /**
     * Transforms an object to a string (id).
     *
     * @param  Object|null $entity
     * @return string
     */
    public function transform($entity)
    {
        if (null === $entity) {
            return "";
        }

        return $entity->getId();
    }

    /**
     * Transforms a string (id) to an object.
     *
     * @param  string $id
     * @return Object|null
     * @throws TransformationFailedException if object is not found.
     */
    public function reverseTransform($id)
    {
        if (!$id) {
            return null;
        }

        $entity = $this->om->getRepository($this->entityClass)->findOneById($id);

        if (null === $entity) {

            throw new TransformationFailedException(sprintf(
                    'An entity of class ' . $this->entityClass . ' with id "%s" does not exist!', $id
            ));
        }

        return $entity;
    }

}

FormType

<?php

namespace Comakai\CQZBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Common\Persistence\ObjectManager;
use Comakai\CQZBundle\Form\DataTransformer\EntityToIdTransformer;

class EntityHiddenType extends AbstractType
{
    /**
     * @var ManagerRegistry
     */
    private $registry;

    /**
     * @var ObjectManager
     */
    private $om;
    private $cache;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ManagerRegistry $registry)
    {
        $this->registry = $registry;
        $this->om = $registry->getManager();
        $this->cache = [];
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $class = (empty($options['data_class'])) ? $this->getClassFromMetadata($builder->getName(), $builder->getParent()->getDataClass()) : $options['data_class'];

        $transformer = new EntityToIdTransformer($this->om, $class);
        $builder->addViewTransformer($transformer);

        $builder->setAttribute('data_class', $class);
    }

    public function getParent()
    {
        return 'hidden';
    }

    public function getName()
    {
        return 'entity_hidden';
    }

    protected function getClassFromMetadata($name, $parentClass)
    {
        /* @var $md \Doctrine\ORM\Mapping\ClassMetadata */
        $md = $this->getMetadata($parentClass)[0];
        $a = $md->getAssociationMapping($name);
        $class = $a['targetEntity'];

        return $class;
    }

    protected function getMetadata($class)
    {
        if (array_key_exists($class, $this->cache)) {
            return $this->cache[$class];
        }

        $this->cache[$class] = null;
        foreach ($this->registry->getManagers() as $name => $em) {
            try {
                return $this->cache[$class] = array($em->getClassMetadata($class), $name);
            } catch (MappingException $e) {
                // not an entity or mapped super class
            } catch (LegacyMappingException $e) {
                // not an entity or mapped super class, using Doctrine ORM 2.2
            }
        }
    }

}

Config (services.yml)

services:
    cqz.form.type.entity_hidden:
        class: Comakai\CQZBundle\Form\Type\EntityHiddenType
        arguments: ["@doctrine"]
        tags:
                -  { name: form.type, alias: entity_hidden }

JobType

$builder->add('company', 'entity_hidden');

Then in your controller

$job = new \Niche\JobBundle\Entity\Job();
$type = new \Niche\JobBundle\Form\JobType();

$job->setCompany($businessUser);

$form = $this->createForm($type, $job);

This way you'll have a reusable entity_hidden type.

UPDATE FOR 2.3

Since there is no $builder->getParent() anymore (https://github.com/symfony/symfony/blob/master/UPGRADE-2.2.md) and because I don't want to set the class of the field's data, I came up with this (btw, now I'm using the form.type_guesser.doctrine service to get the class):

Config

cqz.form.type.suggest:
        class: Comakai\CQZBundle\Form\Type\SuggestType
        arguments: ["@doctrine.orm.entity_manager", "@form.type_guesser.doctrine"]

DataTransformer

<?php

namespace Comakai\CQZBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;

class ObjectToIdTransformer implements DataTransformerInterface
{
     /**
     * @var ObjectManager
     */
    private $om;
    private $objectClass;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om, $objectClass = null)
    {
        $this->om = $om;
        $this->objectClass = $objectClass;
    }

    /**
     * Transforms an object to an id.
     *
     * @param  Object|null $object
     * @return mixed
     */
    public function transform($object)
    {
        if (null === $object) {

            return '';
        }

        return $object->getId();
    }

    /**
     * Transforms an id to an object.
     *
     * @param  mixed $id
     *
     * @return Object|null
     *
     * @throws TransformationFailedException if object is not found.
     */
    public function reverseTransform($id)
    {
        if (!$id) {

            return null;
        }

        $object = $this->om
            ->getRepository($this->objectClass)
            ->find($id)
        ;

        if (null === $object) {

            throw new TransformationFailedException(sprintf(
                'An instance of "%s" with id "%s" does not exist!',
                $this->objectClass,
                $id
            ));
        }

        return $object;
    }

    public function getObjectClass()
    {
        return $this->objectClass;
    }

    public function setObjectClass($class)
    {
        $this->objectClass = $class;
    }
}

Type

<?php

namespace Comakai\CQZBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Comakai\CQZBundle\Form\DataTransformer\ObjectToIdTransformer;

use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;

use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;

class SuggestType extends AbstractType
{
    /**
     * @var ObjectManager
     */
    private $om;
    private $guesser;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om, DoctrineOrmTypeGuesser $guesser)
    {
        $this->om = $om;
        $this->guesser = $guesser;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new ObjectToIdTransformer($this->om);
        $builder->addModelTransformer($transformer);

        if($options['class'] === null) {

            $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($transformer, $builder) {

                /* @var $form \Symfony\Component\Form\Form */
                $form = $event->getForm();
                $class = $form->getParent()->getConfig()->getDataClass();
                $property = $form->getName();
                $guessedType = $this->guesser->guessType($class, $property);
                $options = $guessedType->getOptions();

                $transformer->setObjectClass($options['class']);

            });

        } else {

            $transformer->setObjectClass($options['class']);

        }
    }
    ...

I feel that using a PRE_SET_DATA to set the data class on the transformer is nasty, what do you think?

1
votes

I just created an input type hidden:

$builder->add('child_id', 'hidden', array('mapped' => false))

In the newAction I did fill with the parent id:

$childForm->get('parent_id')->setData($parentEntity->getId());

And finally in createAction I did put:

$child->setParent($em->getReference('MyBundle:Parent', $form["child_id"]->getData()))

PS: I understand that you wanted to create your Data Transformer, but if your problem is persist object with parent id, it will help you.