doums
doums

Reputation: 450

Build Form with Single Table Inheritance

I have my entity Article and one single table inheritance like this :

/**
 * Article
 *
 * @ORM\Table(name="article")
 * @ORM\Entity(repositoryClass="PM\PlatformBundle\Repository\ArticleRepository")
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="media", type="string")
 * @ORM\DiscriminatorMap({"article" = "Article", "movie" = "Movie", "image" = "Image", "text" = "Text"})
 */
class Article
{
    protected $id;
    protected $title;
    protected $description;
    protected $author;
    //other attributes and setters getters
}

class Image extends Article
{
    private $path;
    //getter setter
}

 class Movie extends Article
{
    private $url;
    //getter setter
}

So my article's object type is either Image or movie or text only. Ok now I would like build a form wherein users can post a new article : in this form, the user has to choice between tree type (3 radios button) : image OR movie OR text only and of course the other fields : title and description. How I can do that ? Because with the command

php bin/console doctrine:generate:form myBundle:Article

The form rendered is :

class ArticleType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title',          TextType::class)
            ->add('description',    TextareaType::class)
            ->add('save',           SubmitType::class);
        ;
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'PM\PlatformBundle\Entity\Article'
        ));
    }
}

I don't know the way to implement my STI relation in this form. Because I have not field in my Article entity/object for the type (only in my table). I have to add a Custom ChoiceType() field but it require a attribute. When I try to add this in the form :

        ->add('path',           SearchType::class)
        ->add('url',            UrlType::class)

I got this error :

Neither the property "path" nor one of the methods "getPath()", "path()", "isPath()", "hasPath()", "__get()" exist and have public access in class "PM\PlatformBundle\Entity\Article".

Because I have create an instance of Article, not an instance of Image or Movie. Initially I created a STI thinking a new instance of Article would allow me also to define the "type" of article. But not ? Right ?

Upvotes: 2

Views: 1080

Answers (3)

doums
doums

Reputation: 450

@Boulzy I did this and it works:

    class ArticleController extends Controller
    {
        public function addAction(Request $request)
        {
            $form1 = $this->get('form.factory')->create(TextOnlyType::class);
            $form2 = $this->get('form.factory')->create(ImageType::class);
            $form3 = $this->get('form.factory')->create(MovieType::class);
            return $this->render('PMPlatformBundle:Article:add.html.twig', array(
                'ArticleTextform' => $form1->createView(),
                'ArticleImageform' => $form2->createView(),
                'ArticleMovieform' => $form3->createView(),
            ));
        }

        public function addArticleTextAction(Request $request)
        {
            $ArticleText = new TextOnly;
            $form = $this->get('form.factory')->create(TextOnlyType::class, $ArticleText);
            if ($request->isMethod('POST') && $form->handleRequest($request)->isValid()) {
                $ArticleText->setAuthor(5);
                $ArticleText->setLikes(0);
                $em = $this->getDoctrine()->getManager();
                $em->persist($ArticleText);
                $em->flush();

                $request->getSession()->getFlashBag()->add('notice', 'Annonce bien enregistrée.');

                $listComments = $ArticleText->getComments();
                return $this->render('PMPlatformBundle:Article:view.html.twig', array(
                    'article' => $ArticleText,
                    'listComments' => $listComments
                ));
            }
            return $this->render('PMPlatformBundle:Article:add.html.twig', array(
                'form' => $form->createView(),
            ));
        }

        public function addArticleImageAction(Request $request)
        {
           //the same system as TextOnly
        }

        public function addArticleMovieAction(Request $request)
        {
           //the same system as TextOnly
        }
}

(I override action directly in my template. With POST method.) addAction is my contoller which is called by route for display the view of three forms. As you can see this code works as long as there is no error on submitted form. Because, in this case (when error occured when a form is submitted) each controller needs to return the initial view with the 3 forms. How I can do that ?

Upvotes: 0

Boulzy
Boulzy

Reputation: 456

You will have to make three forms (one for an Article, one for a Movie and one for an Image). Then, in your controller, you have to options to deal with them:

  • Either you use one action to handle the three forms (you can check wich one is submitted by using $form->isSubmitted())
  • You create one action by form, and set the form action URL for each form to the correct controller.

Finally, in your template, you encapsulate your forms in a div, and use the example in my previous post.

{% extends "CoreBundle::layout.html.twig" %}

{% block title %}{{ parent() }}{% endblock %}

{% block btn_scrollspy %}
{% endblock %}


{% block bundle_body %}
    <div class="well">
        <div class="selector">
            <input type="radio" name="form-selector" value="article-form"> Article
            <input type="radio" name="form-selector" value="movie-form"> Movie
            <input type="radio" name="form-selector" value="image-form"> Image
        </div>

        <div class="form article-form" style="display: none;">
            {{ form(articleForm) }}
        </div>
        <div class="form movie-form" style="display: none;">
            {{ form(movieForm) }}
        </div>
        <div class="form image-form" style="display: none;">
            {{ form(imageForm) }}
        </div>
    </div>
{% endblock %}

Upvotes: 1

Boulzy
Boulzy

Reputation: 456

I agree with dragoste in the comments: you can't expect the form to deduce by himself the type of class you want to instantiate based on a value.

Roughly, Image and Movie are the same type as Article, but an Article is not an Image and/or a Movie.

You will have to check that manually. You can do that server side like explained in the comments, with a field using mapped: false to determine the type of entity you need to instantiate, or client side with javascript by using three forms (one for a movie, one for an article, one for an image) and by displaying the correct one based on your radio button.


Edit: How to display the correct form in JS?

I created a JSFiddle to show you how you can do this using jQuery : https://jsfiddle.net/61gc6v16/

With jQuery documentation, you should be able to quickly understand what this sample do, and to adapt it to your needs. :)

Upvotes: 1

Related Questions