Martin Lyne
Martin Lyne

Reputation: 3065

Symfony2 Form with custom FormType calls DataTransformer with same data in both directions

I have made a new FormType and it extends the entity type via

//...
public function getParent()
{
    return 'entity';
}

Which lead my edit form to complain that an integer was not My/Entity/Type and I need a data transformer. So I created one. This is the abbreviated version (it's just the basic tutorial version)

//...
public function reverseTransform($val)
{
    // Entity to int
    return $val->getId();
}
public function transform($val)
{
    // Int to Entity
    return $repo->findOneBy($val);
}
//...

Then added it to my form type

//...
public function buildForm(FormBuilderInterface $builder, array $options)                                                          
{                                                                                                                                 
    $builder->addViewTransformer(new IdToMyModelTransformer($this->em));                                                            
}    

This fixed me viewing my form, but now when I submit the form with an entity picked from my custom widget it tries to call transform not reverseTransform with the $val as an int the ->getId() fails on a non-object.

I can't figure out the correct way of doing this. If I use 'choice' as my widget parent I get a different set of issues (choice default constraints triggered saying it is invalid data?)

I need an entity passed to my widget so it can extract the meta data for display, but I can't post an entity back of course. How do I tell the form that?

Tried setting 'data_class' => null but no joy. Checking network tab shows the value is sent correctly when posting the form.

Update 1

So I re-read the DataTransformer page and that diagram got me thinking, especially after rubber-duck programming above, I ask the form for Entity but expect it to receive ints.. so I actually need a unidirectional transformer, ViewTransformer -> Get entity for display, get posted an int from widget, don't transform it just pass straight through. Which works and I just get the "invalid data" error on update.

Now I have in my Transformer:

public function transform($val)
{
    // Int to Entity
    return $repo->findOneBy($val);
}
public function reverseTransform($val)
{
    // Do nothing
    return $val;
}

Update 2

That seems to have fixed it now, although for some reason if I post int 2 in my form the string "2/" is sent to my transformer. Any ideas on that? FOr now I'm cleaning the string in transformer, but seems like it just shouldnt be happening.

Upvotes: 1

Views: 1052

Answers (2)

Martin Lyne
Martin Lyne

Reputation: 3065

As per my last update I realised, because I was only using my form data to display the currently saved entity relation (the rest is provided by ajax) and not in the same format the form would be receiving it in it lead to some confusion.

To follow the tutorials wording:

Model data

This was all to remain as-is (No model datatransformer needed)

Norm data

No changes

View data (unidirection transformation required)

  • Transform()
    • ID to Entity so widget can access other properties
  • ReverseTransform()
    • Posted ID is in correct format so we just return it

Code

Very simplified:

private $om;
public function __construct (ObjectManager om)
{
    $this->om = $om;
}

public function transform($val)
{
    // Int to Entity
    return $om->getRepository('MyBundle:EntityName')->findOneBy($val);
}

public function reverseTransform($val)
{
    // Do nothing
    return $val;
}

Hopefully that helps anyone else who lets their requirements confuse them!

Upvotes: 1

tftd
tftd

Reputation: 17042

By what I'm seeing in your transformer class you're not implementing the code right. This should be the correct implementation:

namespace App\YourBundle\Form\DataTransformer;

use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

class IdToMyModelTransformer implements DataTransformerInterface
{

    /**
     * @var EntityManager
     */
    private $em;

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

    /**
     * Transforms a value from the original representation to a transformed representation.
     *
     * This method is called on two occasions inside a form field:
     *
     * 1. When the form field is initialized with the data attached from the datasource (object or array).
     * 2. When data from a request is submitted using {@link Form::submit()} to transform the new input data
     *    back into the renderable format. For example if you have a date field and submit '2009-10-10'
     *    you might accept this value because its easily parsed, but the transformer still writes back
     *    "2009/10/10" onto the form field (for further displaying or other purposes).
     *
     * This method must be able to deal with empty values. Usually this will
     * be NULL, but depending on your implementation other empty values are
     * possible as well (such as empty strings). The reasoning behind this is
     * that value transformers must be chainable. If the transform() method
     * of the first value transformer outputs NULL, the second value transformer
     * must be able to process that value.
     *
     * By convention, transform() should return an empty string if NULL is
     * passed.
     *
     * @param mixed $object The value in the original representation
     *
     * @return mixed The value in the transformed representation
     *
     * @throws TransformationFailedException When the transformation fails.
     */
    public function transform($object) {
        if (null === $object) {
            return null;
        }

        return $object->getId();
    }

    /**
     * Transforms a value from the transformed representation to its original
     * representation.
     *
     * This method is called when {@link Form::submit()} is called to transform the requests tainted data
     * into an acceptable format for your data processing/model layer.
     *
     * This method must be able to deal with empty values. Usually this will
     * be an empty string, but depending on your implementation other empty
     * values are possible as well (such as empty strings). The reasoning behind
     * this is that value transformers must be chainable. If the
     * reverseTransform() method of the first value transformer outputs an
     * empty string, the second value transformer must be able to process that
     * value.
     *
     * By convention, reverseTransform() should return NULL if an empty string
     * is passed.
     *
     * @param mixed $categoryId The value in the transformed representation
     *
     * @return mixed The value in the original representation
     *
     * @throws TransformationFailedException When the transformation fails.
     */
    public function reverseTransform($id) {

        if (!$id || $id <= 0) {
            return null;
        }

        if(!ctype_digit($id)){
            throw new TransformationFailedException();
        }

        $repo = $this->em->getRepository('...');
        $result = $repo->findOneBy(array('id' => $id));

        if (null === $result) {
            throw new TransformationFailedException(
                sprintf(
                    'Entity with id does not exist!',
                    $id
                )
            );
        }

        return $result;
    }
}

In the IdToMyIntType you would have something like this:

namespace App\YourBundle\Form\Type;

use App\YourBundle\Form\DataTransformer\IdToMyModelTransformer ;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;


class IdToMyModelType extends AbstractType {

    /**
     * @var EntityManager
     */
    private $em;

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

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

    public function setDefaultOptions( OptionsResolverInterface $resolver ) {
        $resolver->setDefaults(array('invalid_message' => 'Something went wrong message.'));
    }

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

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

I would suggest you check out the DataTransformerInterface and read the documentation over the methods. It'll briefly explain what is that method expected to do. Also, in case you have problems implementing it, you can always check the official documentation, which contains a working example and build up from there.

Upvotes: 1

Related Questions