Symfony6 - Forms and ManyToMany relationship, what field to use? - forms

I'm using Symfony 6.
I have 2 entities 'Article' and 'Picture', with a ManyToMany relationship between both (ie : an Article can have several Picture(s), a Picture can belong to several Article(s));
I made a CRUD:Article and a CRUD:Picture.
Now I when I create a new Article via the form, I would like to have the ability to select what Picture(s) I want to put in my new Article. But I cannot find a way to do that. In the form I only have add->('pictures') in the $builder I can't figure how to set the field to have something like a choiceType where I could select what Picture I want, then save the Article and save the resulting line in the join table...
Any help would be appreciated :)
Sorry for my english (not my native language)
Here my Article entity
namespace App\Form;
use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title')
->add('chapo')
->add('content')
->add('pictures')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
]);
}
}
And my Picture entity:
namespace App\Entity;
use App\Repository\PictureRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PictureRepository::class)]
class Picture
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $altText = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $maxWidth = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $minWidth = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $maxHeight = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $minHeight = null;
#[ORM\Column(length: 255)]
private ?string $picture = null;
#[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'pictures')]
private Collection $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getAltText(): ?string
{
return $this->altText;
}
public function setAltText(?string $altText): self
{
$this->altText = $altText;
return $this;
}
public function getMaxWidth(): ?string
{
return $this->maxWidth;
}
public function setMaxWidth(?string $maxWidth): self
{
$this->maxWidth = $maxWidth;
return $this;
}
public function getMinWidth(): ?string
{
return $this->minWidth;
}
public function setMinWidth(?string $minWidth): self
{
$this->minWidth = $minWidth;
return $this;
}
public function getMaxHeight(): ?string
{
return $this->maxHeight;
}
public function setMaxHeight(?string $maxHeight): self
{
$this->maxHeight = $maxHeight;
return $this;
}
public function getMinHeight(): ?string
{
return $this->minHeight;
}
public function setMinHeight(?string $minHeight): self
{
$this->minHeight = $minHeight;
return $this;
}
public function getPicture(): ?string
{
return $this->picture;
}
public function setPicture(string $picture): self
{
$this->picture = $picture;
return $this;
}
/**
* #return Collection<int, Article>
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
$article->addPicture($this);
}
return $this;
}
public function removeArticle(Article $article): self
{
if ($this->articles->removeElement($article)) {
$article->removePicture($this);
}
return $this;
}
}
And my Form :
namespace App\Form;
use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title')
->add('chapo')
->add('content')
->add('pictures')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
]);
}
}
Finally the controllers
PictureController
namespace App\Controller;
use App\Entity\Picture;
use App\Form\PictureType;
use App\Repository\PictureRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
#[Route('/picture')]
class PictureController extends AbstractController
{
#[Route('/', name: 'app_picture_index', methods: ['GET'])]
public function index(PictureRepository $pictureRepository): Response
{
return $this->render('picture/index.html.twig', [
'pictures' => $pictureRepository->findAll(),
]);
}
#[Route('/new', name: 'app_picture_new', methods: ['GET', 'POST'])]
public function new(Request $request, PictureRepository $pictureRepository,EntityManagerInterface $entityManager): Response
{
$picture = new Picture();
$form = $this->createForm(PictureType::class, $picture, ['add' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$pict = $form->get('picture')->getData();
if($pict)
{
$nomPicture = date('YmdHis') . "-" . uniqid() . "." . $pict->getClientOriginalExtension();
$pict->move(
$this->getParameter('pictures_directory'),
$nomPicture
);
$picture->setPicture($nomPicture);
}
$entityManager->persist($picture);
$entityManager->flush();
$pictureRepository->add($picture, true);
return $this->redirectToRoute('app_picture_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('picture/new.html.twig', [
'picture' => $picture,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_picture_show', methods: ['GET'])]
public function show(Picture $picture): Response
{
return $this->render('picture/show.html.twig', [
'picture' => $picture,
]);
}
#[Route('/{id}/edit', name: 'app_picture_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Picture $picture, PictureRepository $pictureRepository): Response
{
$form = $this->createForm(PictureType::class, $picture);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$pictureRepository->add($picture, true);
return $this->redirectToRoute('app_picture_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('picture/edit.html.twig', [
'picture' => $picture,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_picture_delete', methods: ['POST'])]
public function delete(Request $request, Picture $picture, PictureRepository $pictureRepository): Response
{
if ($this->isCsrfTokenValid('delete'.$picture->getId(), $request->request->get('_token'))) {
$pictureRepository->remove($picture, true);
}
return $this->redirectToRoute('app_picture_index', [], Response::HTTP_SEE_OTHER);
}
}
ArticleController
namespace App\Controller;
use App\Entity\Article;
use App\Form\ArticleType;
use App\Repository\ArticleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/article')]
class ArticleController extends AbstractController
{
#[Route('/', name: 'app_article_index', methods: ['GET'])]
public function index(ArticleRepository $articleRepository): Response
{
return $this->render('article/index.html.twig', [
'articles' => $articleRepository->findAll(),
]);
}
#[Route('/new', name: 'app_article_new', methods: ['GET', 'POST'])]
public function new(Request $request, ArticleRepository $articleRepository): Response
{
$article = new Article();
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$articleRepository->add($article, true);
return $this->redirectToRoute('app_article_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('article/new.html.twig', [
'article' => $article,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_article_show', methods: ['GET'])]
public function show(Article $article): Response
{
return $this->render('article/show.html.twig', [
'article' => $article,
]);
}
#[Route('/{id}/edit', name: 'app_article_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Article $article, ArticleRepository $articleRepository): Response
{
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$articleRepository->add($article, true);
return $this->redirectToRoute('app_article_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('article/edit.html.twig', [
'article' => $article,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_article_delete', methods: ['POST'])]
public function delete(Request $request, Article $article, ArticleRepository $articleRepository): Response
{
if ($this->isCsrfTokenValid('delete'.$article->getId(), $request->request->get('_token'))) {
$articleRepository->remove($article, true);
}
return $this->redirectToRoute('app_article_index', [], Response::HTTP_SEE_OTHER);
}
}

Related

Variable "form" does not exist. Symfony 4

I am new to Symfony 4 and I am following this tutorial "https://symfony.com/doc/4.4/forms.html" but it does not recognize my variable "form" and I don't know why (everything is up to date).
My Controller :
<?php
namespace App\Controller;
use App\Entity\Task;
use App\Form\Type\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
class TaskController extends AbstractController
{
public function new(Request $request)
{
// creates a task object and initializes some data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));
/*$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, ['label' => 'Create Task'])
->setMethod('GET')
->getForm();*/
$form = $this->createForm(TaskType::class, $task);
return $this->render('search/search.html.twig', [
'form' => $form->createView(),
]);
}
}
My TaskType.php :
<?php
namespace App\Form\Type;
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
And my Task.php :
<?php
namespace App\Entity;
class Task
{
protected $task;
protected $dueDate;
public function getTask()
{
return $this->task;
}
public function setTask($task)
{
$this->task = $task;
}
public function getDueDate()
{
return $this->dueDate;
}
public function setDueDate(\DateTime $dueDate = null)
{
$this->dueDate = $dueDate;
}
}
If you see any error from my part I am open..
I am calling my form here :
{{ form_start(form) }}
{{ form(form) }}
{{ form_end(form) }}
In my "search.html.twig".

Bootstrap TagsInput with EntityType dropdown values (Symfony)

I'm making a form to register books (Symfony 4), each with a unique publisher (ManyToOne) and one or many authors (ManyToMany). Both fields are filled in the form as tags, with Bootstrap TagsInput, so that if the user types in those fields, they will suggest the list of values in the DB tables, and if not, the new values will be inserted when submitting, along with the other book data.
I followed the official example of Best Practices on the Symfony website, and the tagging system works for authors, because it's a CollectionType, but not with the publisher, because it's an EntityType. How could I adapt it? Fails to transform publisher values into comma-separated strings so it can be recognized by TagsInput.
Book Entity (App\Entity\Book.php)
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
class Book
{
/**
* #var \Publisher
*
* #ORM\ManyToOne(targetEntity="Publisher", cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(nullable=false, name="publisher_id", referencedColumnName="id")
* })
*/
private $publisher;
/**
* #var Author[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Author", cascade={"persist"})
* #ORM\JoinTable(name="authors_books")
*/
private $authors;
public function __construct()
{
$this->authors = new ArrayCollection();
}
public function getPublisher()
{
return $this->publisher;
}
public function getAuthors(): Collection
{
return $this->authors;
}
}
Book Form (App\Form\BookType.php)
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use App\Form\Type\PublisherType;
use App\Form\Type\AuthorType;
class BookType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->add('publisher', PublisherType::class, array(
'label' => 'Publisher',
))
->add('authors', AuthorType::class, array(
'label' => 'Author/s'
))
}
}
AuthorType
namespace App\Form\Type;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use App\Form\DataTransformer\AuthorToStringTransformer;
use App\Repository\AuthorRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class AuthorType extends AbstractType {
private $authors;
public function __construct(AuthorRepository $authors_repo)
{
$this->authors = $authors_repo;
}
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->addModelTransformer(new CollectionToArrayTransformer(), true)
->addModelTransformer(new AuthorToStringTransformer($this->authors), true)
;
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['authors'] = $this->authors->findAll();
}
public function getParent()
{
return TextType::class;
}
}
PublisherType
namespace App\Form\Type;
use App\Form\DataTransformer\EntityToArrayTransformer;
use App\Form\DataTransformer\PublisherToStringTransformer;
use App\Repository\PublisherRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class PublisherType extends AbstractType {
private $publishers;
public function __construct(PublisherRepository $publisher_repo) {
$this->publishers = $publisher_repo;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->addModelTransformer(new EntityToArrayTransformer(), true)
->addModelTransformer(new PublisherToStringTransformer($this->publishers), true);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$publishers = $this->publishers->findAll();
}
public function getParent()
{
return TextType::class;
}
}
CollectionToArrayTransformer
namespace Symfony\Bridge\Doctrine\Form\DataTransformer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\DataTransformerInterface;
class CollectionToArrayTransformer implements DataTransformerInterface
{
public function transform($collection)
{
if (null === $collection) {
return [];
}
if (\is_array($collection)) {
return $collection;
}
return $collection->toArray();
}
}
EnityToArrayTransformer
namespace App\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
class EntityToArrayTransformer implements DataTransformerInterface
{
public function transform($entity)
{
if (null === $entity) {
return [];
}
return [$entity];
}
}
AuthorToStringTransformer
namespace App\Form\DataTransformer;
use App\Entity\Author;
use App\Repository\AuthorRepository;
use Symfony\Component\Form\DataTransformerInterface;
class AuthorToStringTransformer implements DataTransformerInterface
{
private $authors;
public function __construct(AuthorRepository $authors)
{
$this->authors = $authors;
}
public function transform($authors): string
{
/* #var Author[] $authors */
return implode(',', $authors);
}
public function reverseTransform($string): array
{
...
}
}
PublisherToStringTransformer
namespace App\Form\DataTransformer;
use App\Entity\Publisher;
use App\Repository\PublisherRepository;
use Symfony\Component\Form\DataTransformerInterface;
class PublisherToStringTransformer implements DataTransformerInterface
{
private $publishers;
public function __construct(PublisherRepository $publishers)
{
$this->publishers = $publishers;
}
public function transform($publisher): string
{
/* #var Publisher[] $publisher */
return implode(',', $publisher);
}
public function reverseTransform($publisher): string
{
...
}
}
Form Twig
{{ form_widget(form.publisher, {'attr': {'class': class, 'data-toggle': 'tagsinput', 'data-publishers': form.publisher.vars.publishers|json_encode } }) }}
{{ form_widget(form.publisher, {'attr': {'class': class, 'data-toggle': 'tagsinput', 'data-authors': form.publisher.vars.authors|json_encode } }) }}
This is the code I use for Editors and Authors, so that you can compare the one that works and the one that doesn't work. They are not so different, but something is wrong, and I don't know what it is or what I need to change.
Ok, I've found a solution. I have made all the changes in both Transformers, because the problem was the format to which I converted the entity and vice versa.

How to unit test a Symfony2 form when it uses a transformer linked to a database

TLDR: I am new to unit tests and I have few questions:
Are my transformer tests well written?
Is there a way to decoupled my transformer tests from the database?
How to test my form with the transformer using the database?
Should I decouple my form from my transformer?
I don't know if my classes are too coupled, if my design is flawed or if my understanding of the unit tests is bad.
Here is some background.
I have a form object with different widgets. One of them is used within a model transformer.
This model transformer uses a connection to the database to retrieve the proper object.
Here is my code:
class BookToStringTransformer implements DataTransformerInterface {
private $om;
public function __construct(ObjectManager $om) {
$this->om = $om;
}
public function transform($book) {
if (!$book instanceof Book) {
return "";
}
return $book->getName();
}
public function reverseTransform($string) {
if (!is_string($string) || !$string) {
return null;
}
$book = $this->om
->getRepository('MyBundle:Book')
->findOneBy(array('name' => $string))
;
if (null === $book) {
throw new TransformationFailedException(sprintf(
'The book "%s" does not exist!', $string
));
}
return $book;
}
}
class ItemType extends AbstractType {
private $om;
public function __construct(ObjectManager $om) {
$this->om = $om;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$bookTransformer = new BookToStringTransformer($this->om);
$builder->add($builder->create('book', 'text', array(
'required' => false,
))->addModelTransformer($bookTransformer));
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Item',
));
}
public function getName() {
return 'mybundle_item';
}
}
I wrote unit tests for the transformer using the KernelTestCase
class BookToStringTransformerTest extends KernelTestCase {
private $name = 'existing name';
private $em;
public function setUp() {
static::$kernel = static::createKernel();
static::$kernel->boot();
$this->em = static::$kernel->getContainer()
->get('doctrine')
->getManager();
}
public function testReverseTransform_whenNameExists_returnsBookObject() {
$transformer = new BookToStringTransformer($this->em);
$book = $transformer->reverseTransform($this->name);
$this->assertInstanceOf('MyBundle\Entity\Book', $book, 'Should return a Book object');
$this->assertEquals($this->name, $book->getName(), 'Should return a Book object with the selected name');
}
/**
* #expectedException Symfony\Component\Form\Exception\TransformationFailedException
*/
public function testReverseTransform_whenNameDoesNotExist_throwsException() {
$transformer = new BookToStringTransformer($this->em);
$transformer->reverseTransform('unknown name');
}
/**
* #param mixed $invalid_parameter
* #dataProvider provideInvalidParameter
*/
public function testReverseTransform_whenParameterIsInvalid_returnsNull($invalid_parameter) {
$om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock();
$transformer = new BookToStringTransformer($om);
$this->assertNull($transformer->reverseTransform($invalid_parameter), 'Should return a NULL value');
}
/**
* #return array
*/
public function provideInvalidParameter() {
return [
[null],
[false],
[true],
[''],
[[]],
[new \stdClass()],
];
}
public function testTransform_whenParameterIsBookObject_returnsName() {
$book = $this->em->getRepository('MyBundle:Book')
->findOneBy(array('name' => $this->name));
$om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock();
$transformer = new BookToStringTransformer($om);
$this->assertEquals($this->name, $transformer->transform($book), 'Should return a string containing the name');
}
/**
* #param mixed $not_book
* #dataProvider provideInvalidBookObject
*/
public function testTransform_whenParameterIsNotBookObject_returnsEmptyString($not_book) {
$om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock();
$transformer = new BookToStringTransformer($om);
$this->assertEquals("", $transformer->transform($not_book), 'Should return an empty string to be chained');
}
/**
* #return array
*/
public function provideInvalidBookObject() {
return [
[null],
[123],
['123'],
[[]],
[true],
[new \stdClass()],
];
}
}
As I am new to unit tests, I don't even know if it is the proper way to test that transformer.
I start writing tests for the form object. I am using the TypeTestCase but there is no simple way to get the connection to the database and I can't use the KernelTestCase.
class ItemTypeTest extends TypeTestCase {
/**
* #expectedException \PHPUnit_Framework_Error
*/
public function test_whenCreatedWithNoParameters_raiseException() {
new ItemType();
}
/**
* #expectedException \PHPUnit_Framework_Error
*/
public function test_whenCreatedWithBadParameters_raiseException() {
new ItemType(123);
}
public function test_whenCreatedWithGoodParameters_createsFormObject() {
$om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock();
$type = new ItemType($om);
$form = $this->factory->create($type);
$this->assertInstanceOf('Symfony\Component\Form\Form', $form);
}
public function test_whenSubmittedWithGoodData() {
$formData = array(
'name' => 'existing name',
);
$om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock();
$type = new ItemType($om);
$form = $this->factory->create($type);
$form->submit($formData);
}
}
The last test fails because the transformer does get access to the database since I am passing a mock to the form. So should I get a real object (meaning classes are too coupled) or should I find an other way.
Thank you
The approach is good, in the last method you must mock the repo object and the repo response. In example try this code:
public function test_whenSubmittedWithGoodData() {
$formData = array(
'name' => 'existing name',
);
$om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock();
$repoMock= $this->getMock('Doctrine\ORM\EntityRepository', array(), array(), '', false);
$om
->expects($this->atLeastOnce())
->method('getRepository')
->withAnyParameters()
->will($this->returnValue($repoMock));
$repoMock
->expects($this->atLeastOnce())
->method('findOneBy')
->withAnyParameters()
->will($this->returnValue($mockedBook));
$type = new ItemType($om);
$form = $this->factory->create($type);
$form->submit($formData);
}

FOSUserBundle : Redirect the user after register with EventListener

I want to redirect the user to another form just after registration, before he could access to anything on my website (like in https://github.com/FriendsOfSymfony/FOSUserBundle/issues/387).
So I create an eventListener like in the doc :
<?php
namespace rs\UserBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\UserEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Listener responsible to change the redirection at the end of the password resetting
*/
class RegistrationConfirmedListener implements EventSubscriberInterface
{
private $router;
public function __construct(UrlGeneratorInterface $router)
{
$this->router = $router;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationConfirmed'
);
}
public function onRegistrationConfirmed()
{
$url = $this->router->generate('rsWelcomeBundle_check_full_register');
$response = new RedirectResponse($url);
return $response;
}
}
Services.yml :
services:
rs_user.registration_completed:
class: rs\UserBundle\EventListener\RegistrationConfirmedListener
arguments: [#router]
tags:
- { name: kernel.event_subscriber }
But it doesn't work, the user register, he click on the confirmation link in his mailbox, he is not redirected on the page I want, he is logged and I just have the message who said the account is confirmed.
Why it doesn't redirect me to the route : rsWelcomeBundle_check_full_register like I want ?
Thanks
To accomplish what you want, you should use FOSUserEvents::REGISTRATION_CONFIRM instead of FOSUserEvents::REGISTRATION_CONFIRMED.
You then have to rewrite rewrite your class RegistrationConfirmedListener like:
class RegistrationConfirmListener implements EventSubscriberInterface
{
private $router;
public function __construct(UrlGeneratorInterface $router)
{
$this->router = $router;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRM => 'onRegistrationConfirm'
);
}
public function onRegistrationConfirm(GetResponseUserEvent $event)
{
$url = $this->router->generate('rsWelcomeBundle_check_full_register');
$event->setResponse(new RedirectResponse($url));
}
}
And your service.yml:
services:
rs_user.registration_complet:
class: rs\UserBundle\EventListener\RegistrationConfirmListener
arguments: [#router]
tags:
- { name: kernel.event_subscriber }
REGISTRATION_CONFIRM receives a FOS\UserBundle\Event\GetResponseUserEvent instance as you can see here: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/FOSUserEvents.php
It allows you to modify the response that will be sent: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Event/GetResponseUserEvent.php
"friendsofsymfony/user-bundle": "2.0.x-dev",
Not sure why the accepted answer works for you as REGISTRATION_CONFIRM happens after the token is confirmed.
In case you want to perform an action, redirect to another page with some additional form after the FOS registerAction I would suggest the following way.
This is the code that is performed on registerAction once the submitted form is valid by FOS:
FOS\UserBundle\Controller\RegistrationController
if ($form->isValid()) {
$event = new FormEvent($form, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);
$userManager->updateUser($user);
if (null === $response = $event->getResponse()) {
$url = $this->generateUrl('fos_user_registration_confirmed');
$response = new RedirectResponse($url);
}
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
As you can see the first possible return happens after FOSUserEvents::REGISTRATION_SUCCESS event in case the response is null which in my case doesn't as I have configured a mailer to send a confirmation token and FOS is using an listener that listens to this FOSUserEvents::REGISTRATION_SUCCESS event and after sending an email it sets a redirect response.
FOS\UserBundle\EventListener\EmailConfirmationListener
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
/**
* #param FormEvent $event
*/
public function onRegistrationSuccess(FormEvent $event)
{
/** #var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$user->setEnabled(false);
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
$this->mailer->sendConfirmationEmailMessage($user);
$this->session->set('fos_user_send_confirmation_email/email', $user->getEmail());
$url = $this->router->generate('fos_user_registration_check_email');
$event->setResponse(new RedirectResponse($url));
}
Okay I understand! So how do I redirect to another page?
I would suggest to overwrite checkEmailAction as most likely you don't want to overwrite the listener that sends an email as that's part of your workflow.
Simply:
TB\UserBundle\Controller\RegistrationController
/**
* #return \Symfony\Component\HttpFoundation\Response
*/
public function checkEmailAction()
{
/** #var UserManager $userManager */
$userManager = $this->get('fos_user.user_manager');
/** #var string $email */
$email = $this->get('session')->get('fos_user_send_confirmation_email/email');
$user = $userManager->findUserByEmail($email);
return $this->redirect($this->generateUrl('wall', ['username' => $user->getUsername()]));
}
As you can see instead of rendering FOS's check_email template I decided to redirect user to his new profile.
Docs how to overwrite an controller: https://symfony.com/doc/master/bundles/FOSUserBundle/overriding_controllers.html (basically define a parent for your bundle and create a file in the directory with the same name as FOS does.)
Route redirection can also be used:
fos_user_registration_confirmed:
path: /register/confirmed
defaults:
_controller: FrameworkBundle:Redirect:redirect
route: redirection_route
permanent: true
If you're not using a confirmation email, you can redirect the user right after submiting the registration form this way :
class RegistrationConfirmationSubscriber implements EventSubscriberInterface
{
/** #var Router */
private $router;
public function __construct(Router $router)
{
$this->router = $router;
}
public static function getSubscribedEvents()
{
return [FOSUserEvents::REGISTRATION_COMPLETED => 'onRegistrationConfirm'];
}
public function onRegistrationConfirm(FilterUserResponseEvent $event)
{
/** #var RedirectResponse $response */
$response = $event->getResponse();
$response->setTargetUrl($this->router->generate('home_route'));
}
}
The subscriber declaration stay the same :
registration_confirmation_subscriber:
class: AppBundle\Subscriber\RegistrationConfirmationSubscriber
arguments:
- "#router"
tags:
- { name: kernel.event_subscriber }
For a quick solution: you can also override the route. Let's say you want to redirect to your homepage you can do something like this:
/**
* #Route("/", name="index")
* #Route("/", name="fos_user_registration_confirmed")
* #Template(":Default:index.html.twig")
*/
public function indexAction()
{

Symfony2: Adding a collection based on a table-inheritance structure to a FormView

I am working on a Symfony2/Doctrine app which uses class-table-inheritance (http://docs.doctrine-project.org/en/2.0.x/reference/inheritance-mapping.html#class-table-inheritance) to manage Complaints in a Consult. Each Consult can have many Complaints (OneToMany), and each different type of Complaint has a different structure and appearance. Complaints are a collection and are added dynamically with JS.
At this point, I am able to persist the Complaints and link them to the Consults by recasting them as the appropriate types in the Controller before persistence. I have run into some issues with this and I'm planning on migrating this to a form event (http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html) or something of that nature to streamline the process.
The problem that I am running into at this point, however, is that I am unable to display existing Complaints in a view using the FormView because the form builder demands that I set the type of the collection to be displayed. If each Consult had only one type of Complaint, that would be fine, but they can have multiple types, and setting the type in the form builder limits me to that one type.
Is there some approach that I can take to stop the FormView from tyring to convert to string in the absence of a type or some way to dynamically detect and assign the type on a per-Complaint basis (using $complaint->getComplaintType(), perhaps)?
<?php
namespace Acme\ConsultBundle\Entity;
class Consult
{
/**
* #ORM\OneToMany(targetEntity="Acme\ConsultBundle\Entity\ComplaintBase", mappedBy="consult", cascade={"persist", "remove"})
*/
protected $complaints;
}
?>
<?php
namespace Acme\ConsultBundle\Entity;
/**
* Acme\ConsultBundle\Entity\ConsultBase
*
* #ORM\Entity
* #ORM\Table(name="ConsultComplaintBase")
* #ORM\HasLifecycleCallbacks
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="complaint_name", type="string")
* #ORM\DiscriminatorMap({
* "ComplaintDefault" = "Acme\ConsultBundle\Entity\ComplaintDefault",
* "ComplaintRosacea" = "Acme\ConsultBundle\Entity\ComplaintRosacea",
* "ComplaintBotox" = "Acme\ConsultBundle\Entity\ComplaintBotox",
* "ComplaintAcne" = "Acme\ConsultBundle\Entity\ComplaintAcne",
* "ComplaintUrticaria" = "Acme\ConsultBundle\Entity\ComplaintUrticaria",
* })
*/
abstract class ComplaintBase
{
/**
* #ORM\ManyToOne(targetEntity="Acme\ConsultBundle\Entity\Consult", inversedBy="complaints")
* #ORM\JoinColumn(name="consult_id", referencedColumnName="id")
*/
protected $consult;
/**
* #ORM\Column(type="string", length="255")
*/
protected $complaintType;
}
?>
<?php
namespace Acme\ConsultBundle\Form\Type;
class ConsultType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('complaints', 'collection', array(
// 'type' => new ComplaintUrticariaType(),
'error_bubbling' => true,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
));
}
}
?>
Not exactly sure that it will work with the collection, but it certainly works with a single form. Please try this idea.
First make a form for your basic entity ComplaintBase
class ComplaintForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$subscriber = new ComplaintSubscriber($builder);
$builder->addEventSubscriber($subscriber);
/* your fields */
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\ConsultBundle\Entity\ComplaintBase',
));
}
}
Then in subscriber you can define additional fields based on submitted entity type.
class ComplaintSubscriber implements EventSubscriberInterface
{
private $factory;
private $builder;
public function __construct(FormBuilderInterface $builder)
{
$this->factory = $builder->getFormFactory();
$this->builder = $builder;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
);
}
public function preSetData(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$class = get_class($data);
if( $class === 'Acme\ConsultBundle\Entity\ComplaintDefault' ) {
$this->processDefault($data, $form);
}
elseif( $class === 'Acme\ConsultBundle\Entity\ComplaintRosacea' ) {
$this->processRosacea($data, $form);
}
elseif( $class === 'Acme\ConsultBundle\Entity\ComplaintBotox' ) {
$this->processBotox($data, $form);
}
else {
#NOP
}
}
protected function processDefault(Entity\ComplaintDefault $node, FormInterface &$form)
{
#NOP
}
protected function processRosacea(Entity\ComplaintRosacea $node, FormInterface &$form)
{
$form->add($this->factory->createNamed('some_field', 'text'));
}
protected function processBotox(Entity\ComplaintBotox $node, FormInterface &$form)
{
$form->add($this->factory->createNamed('other_field', 'text'));
}
}