Skip to content

Instantly share code, notes, and snippets.

@webmozart
Created March 23, 2011 15:32
Show Gist options
  • Save webmozart/883293 to your computer and use it in GitHub Desktop.
Save webmozart/883293 to your computer and use it in GitHub Desktop.
<?php
protected function _editAction(Post $post)
{
$em = $this->get('doctrine.orm.default_entity_manager');
$factory = $this->get('form.factory');
$form = $factory->create(new PostFormType());
$form->setData($post);
if ($this->get('request')->getMethod() === 'POST') {
$form->bindRequest($this->get('request'));
if ($form->isValid()) {
$em->persist($post);
$em->flush();
return new RedirectResponse($this->generateUrl('hello_index'));
}
}
return $this->render('MyHelloBundle:Hello:edit.html.twig', array(
'form' => $form->createView(),
));
}
<?php
namespace My\HelloBundle\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Type\AbstractType;
use My\HelloBundle\Entity\Comment;
class PostFormType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title')
->add('file')
->add('content')
->add('abstract')
->add('enabled')
->add('publicationDateStart', 'date')
->add('commentsDefaultStatus', 'choice', array(
'choices' => Comment::getStatusCodes(),
))
// the second parameter, $type, is null because we use auto-creation
->add('tags', null, array(
'expanded' => true,
'multiple' => true,
))
->build('author', 'form', array('data_class' => 'My\HelloBundle\Entity\Author'))
->add('firstName')
->add('lastName')
->end();
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'My\HelloBundle\Entity\Post',
);
}
public function getName()
{
return 'postform';
}
}
{% form_theme form _self %}
{{ form_enctype(form) }}
{{ form_widget(form) }}
{{ form_widget(form.firstName) }}
{% for child in form %}
{{ form_widget(child) }}
{% endfor %}
{{ form_widget(form.firstName, { 'attr': { 'class': 'foobar' } }) }}
{{ form_label(form.firstName, 'My label') }}
@webmozart
Copy link
Author

Done

@ubermuda
Copy link

I though symfony2 was all about removing the magic?

@webmozart
Copy link
Author

You can provide the type and options explicitely for every field, if you want. This short syntax is meant for not duplicating logic.

Say, you store a property as datetime with constraint notnull. Symfony already knows that you (most probably) want a required date field, wo why duplicate its definition here?

@ubermuda
Copy link

That part of magic is, of course, ok, but when you explicitly specify a field type, how does it work behind the scene? I've not read the code but I imagine the date in ->add('publicationDateStart', 'date') gets inflected to some class name?

@webmozart
Copy link
Author

You see the getName() method in the PostFormType class? Every type has a name which is used to retrieve that type from the DIC (or wherever types are instantiated). So in your case the type with the name "date" (which is DateType) is used to configure the added field.

You can also explicitely pass the type object

->add('publicationDateStart', new DateType(...))

but then you need to know all constructor arguments of all types and get them from somewhere. By passing the name the DIC can do that for you.

@brikou
Copy link

brikou commented Apr 2, 2011

I've fork AcmePizzaBundle you can take a look here, https://github.com/brikou/AcmePizzaBundle, I've tried to improve/correct this bundle, take a look and give me feedbak ;)

@brikou
Copy link

brikou commented Apr 15, 2011

@bschussek Can you update your gist with latest usage, because it is obscure to me (this way I'll update the AcmeDemoBundle) ;) thx

@webmozart
Copy link
Author

@brikou: Done.

@brikou
Copy link

brikou commented Apr 15, 2011

@bschussek great thx

@boutell
Copy link

boutell commented Apr 15, 2011

What does this do exactly?

->build('author', 'form', array('data_class' => 'My\HelloBundle\Entity\Author'))

Is that a subform that creates a new author object for every post? Does that make sense?

Just trying to follow it through (:

@webmozart
Copy link
Author

@boutell: It embeds a new FormBuilder and immediately lets you configure it. So

$builder->build('author', 'form')
    ->add('firstName', 'text')
    ->add('lastName', 'text)
->end()

is a shorthand notation for

$authorBuilder = $factory->createBuilder('form', 'author')
    ->add('firstName', 'text')
    ->add('lastName', 'text);

$builder->add($authorBuilder);

Can you think of a more intuitive naming for build()?

@beberlei
Copy link

addEmbedded or something along this lines?

@stloyd
Copy link

stloyd commented Apr 15, 2011

embed :) or maybe include (also longer but IMO more intuitive: embedForm/includeForm).

@boutell
Copy link

boutell commented Apr 15, 2011 via email

@webmozart
Copy link
Author

@boutell: You get a new FormBuilder object with the name "author", as in the above code snippet.

@boutell
Copy link

boutell commented Apr 15, 2011 via email

@webmozart
Copy link
Author

Hehe, ok :) We're not dealing with Doctrine2 in any way here. We're just embedding a form into another. You're right that editing the data of an author within a post form does not make much sense - it was merely an example.

Long story short, you end up with a post form with lots of fields and an embedded form "author" with the fields "firstName" and "lastName". Does that make it clearer?

@boutell
Copy link

boutell commented Apr 15, 2011 via email

@marijn
Copy link

marijn commented Apr 19, 2011

Looking good...
Though I would prefer if data_class was extracted in the form via get_class() seems weird to ask the user about it...
Just my 2cents

@webmozart
Copy link
Author

@marijn: Unfortunately that's not possible. First of all, your application might provide invalid data of a wrong class or a non-object. Second, in many cases a form doesn't even have an object (think "create" forms), and we still want to be able to guess the types for those. In the above example, what if the $author property of the post is empty?

@marijn
Copy link

marijn commented Apr 20, 2011

Well I think the API would be simpler if we would require to inject empty objects in that case...

For example

$form = $factory->create(new PostFormType());
$form->setData(new Post());

Though I'm not sure how that would work out for forms based on arrays...
Those should provide a static method for validation right? So we wouldn't need the class name in that case...

Maybe I'm missing the point here but I'm not convinced of the design requirement of the data_class option...

@webmozart
Copy link
Author

@marijn: In such a case you would also have to manually prefill the relations of an object, if you use embedded forms.

$post = new Post();
if (!$post->author) {
    $post->author = new Author();
}
if (!$post->author->address) {
    $post->author->address = new Address();
}
$form->setData($post);

You could avoid this mess by setting the empty data option:

class MyEmailType
{
    public function getDefaultOptions()
    {
        return array(
            // can also be a plain object
            'empty_data' => function (FormInterface $form) {
                return new Email(...);
            }
        );
    }
}

But then we have the problem that we need to invoke this closure already when building the form in order to find out the type of the created object, which we can't, because we don't have a FormInterface object yet, only a FormBuilder.

@marijn
Copy link

marijn commented Apr 20, 2011 via email

@docteurklein
Copy link

How do you handle something kind like EntityChoiceList ? do I have to create my "choices" option array manually ?

@webmozart
Copy link
Author

@docteurklein Care to elaborate your question?

@docteurklein
Copy link

Sorry if misunderstood :)

I want my form to display a select box corresponding to all the elements of a mongo database collection.
For that, I get an array of Documents and pass it to my choice type via the choices options.

the problem is that the dataTransformer attached to the choice type is a ScalarToChoiceTransformer and it can't handle Object transformation.

FormType:

        $builder->add('frontend_template', 'choice', array(
            'choices' => $this->getTemplateChoices() // this is an array containing Objects
        ))

@docteurklein
Copy link

I finally implemented my own DataTransformer and replaced the Scalar one.

Here is how I did:

$builder->get('frontend_template')->resetClientTransformers();
$builder->get('frontend_template')->appendClientTransformer(new DocumentToChoiceTransformer($this->dm, 'Test\Document\Template'));

Transformer:

<?php

namespace Test\BlockBundle\Form;

use Doctrine\ODM\MongoDB\DocumentManager;

use Symfony\Component\Form\DataTransformerInterface;
use Test\Form\ToIdTransformable;

class DocumentToChoiceTransformer implements DataTransformerInterface
{
    private $dm;
    private $className;

    public function __construct(DocumentManager $dm, $className) 
    {
        $this->dm = $dm;
        $this->className = $className;
    }

    public function transform($value)
    {
        if(null === $value) {

            return '';
        }

        if(is_scalar($value)) {
            return $this->dm->getRepository($this->className)->find($value);
        }

        if( ! $value instanceof ToIdTransformable) {
            throw new \InvalidArgumentException(sprintf('Object must implement "ToIdTransformable" interface'));
        }

        return $value->getId();
    }

    public function reverseTransform($value)
    {
        if(is_scalar($value)) {
            return $this->dm->getRepository($this->className)->find($value);
        }
    }
}

@hectorh30
Copy link

very helpful! thanks

@docteurklein
Copy link

I forgot to say that your domain objects must implement the ToIdTransformable interface:

interface ToIdTransformable
{
    function getId();
}

@docteurklein
Copy link

But I really would like to have the thoughts of @bschussek on this :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment