Symfony : OrderBy in CollectionType - forms

I have two tables (Player & Historique) that have a OneToMany association. In my PlayerType form, I have a CollectionType with an entry_type to my HistoriqueType. My question is about the order in which the data from HistoriqueType arrives. For the moment, it appears in ASC order of the id. But I would like it to appear in ASC order of the years (season).
Here are my two entities :
<?php
/**
* #ORM\Entity(repositoryClass=PlayerRepository::class)
*/
class Player
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity=Historique::class, mappedBy="player", cascade={"persist"}, orphanRemoval = true)
*/
public $playerHistoriques;
public function __construct()
{
$this->playerHistoriques = new ArrayCollection();
}
And my Historique Class :
<?php
/**
* #ORM\Entity(repositoryClass=HistoriqueRepository::class)
*/
class Historique
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=Club::class, inversedBy="historiques")
* #ORM\JoinColumn(nullable=true)
*/
private $club;
/**
* #ORM\ManyToOne(targetEntity=Season::class, inversedBy="historiques")
* #ORM\JoinColumn(nullable=true)
* #ORM\OrderBy({"season" = "ASC"})
*/
private $season;
/**
* #ORM\ManyToOne(targetEntity=Player::class, inversedBy="playerHistoriques")
* #ORM\JoinColumn(nullable=true)
*/
private $player;
/**
* #ORM\ManyToOne(targetEntity=Position::class, inversedBy="historiques")
*/
private $position;
My PlayerType :
<?php
class PlayerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('playerHistoriques', CollectionType::class, [
'entry_type' => HistoriqueType::class,
'entry_options' => [
'label' => false
],
'by_reference' => false,
'allow_add' => true
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Player::class,
]);
}
}
And My HistoriqueType:
<?php
class HistoriqueType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('club', EntityType::class, [
'class' => Club::class,
'choice_label' => 'name',
'label' => false,
'required' => false,
'placeholder' => '-',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.name', 'ASC');
}
])
->add('season', EntityType::class, [
'class' => Season::class,
'choice_label' => 'year',
'label' => false,
'placeholder' => '-',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('s')
->orderBy('s.year', 'ASC');
}
])
->add('position', EntityType::class, [
'class' => Position::class,
'choice_label' => 'position',
'label' => false,
'placeholder' => '-',
'required' => false
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Historique::class,
'method' => 'get',
'csrf_protection' => false
]);
}
public function getBlockPrefix() {
return '';
}
}
In my edit form, I would like to order my collectionType by 'season' => 'ASC', in order to have the years in chronological order even in my edit form.
I have tried a query_builder like so in my PlayerType :
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('h')
->orderBy('h.season', 'ASC');
}
But it generated an error since collectionTypes cannot have queries in them.
I have tried to order it automatically in my PlayerEntity #ORM\OrderBy({"season" = "ASC"}) but it didn't work.

In your Historique Entity change the order by to year instead of season (I believe in your other form you are sorting by the year so hopefully this is the property you are looking to sort by).
/**
* #ORM\ManyToOne(targetEntity=Season::class, inversedBy="historiques")
* #ORM\JoinColumn(nullable=true)
* #ORM\OrderBy({"year" = "ASC"})
*/
private $season;
You should be able to remove the query builder now, unless you need to narrow the selection.

OrderBy must be in Palyer entity in OneToMany relation not in Historique
/**
*#ORM\OneToMany(targetEntity=Historique::class, mappedBy="player", cascade={"persist"}, orphanRemoval = true)
*#ORM\OrderBy({"season" = "ASC"})
*/
public $playerHistoriques;

Related

Symfony Check if at least one of two fields isn't empty on form validation of CollectionType

In a previous question (Symfony Check if at least one of two fields isn't empty on form validation) I had asked help for form validation using Callback. The answer given by #hous was right, but it doesn't work for elements in a CollectionType, reason why I'm opening a new question.
Based on the previous answer I have done the following:
Here is my "mother" Form:
class BookingVisitorType extends AbstractType
{
private $router;
private $translator;
public function __construct()
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('visitors', CollectionType::class, [
'entry_type' => VisitorType::class,
'label' => 'entity.booking.visitors',
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'by_reference' => false,
'entry_options' => [
'label' => false,
'delete-url' => $options['visitor-delete-url']
],
'constraints' =>[
new Count([
'min' => 1,
'minMessage' => 'validator.visitor.at-least-one-visitor',
'max' => $options['numberOfPlaces'],
'maxMessage' => 'validator.visitor.cannot-have-more-visitor-than-spaces',
'exactMessage' => 'validator.visitor.exact-message'
])
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Booking::class,
'numberOfPlaces' => 1,
'visitor-delete-url' => ''
]);
}
}
Here is my "son" Form:
class VisitorType extends AbstractType
{
private $phone;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class, [
'label' => 'entity.visitor.first-name',
'constraints' => [
new NotBlank(),
new Length([
'min' => 2,
'max' => 255
]),
new Regex([
'pattern' => "/[\pL\s\-]*/",
'message' => 'validator.visitor.not-valide-first-name'
])
]
])
->add('phone', TextType::class, [
'label' => 'entity.visitor.phone-number',
'required' => false,
'constraints' => [
new Regex([
'pattern' => "/[0-9\s\.\+]*/",
'message' => 'validator.visitor.not-valide-phone-number'
]),
new Callback(function($phone, ExecutionContextInterface $context){
$this->phone = $phone;
}),
]
])
->add('email', TextType::class, [
'label' => 'entity.visitor.email',
'required' => false,
'constraints' => [
new Email(),
new Callback(function($email, ExecutionContextInterface $context){
if ($this->phone == null && $email == null) {
$context->buildViolation('validator.visitor.email-or-phone-required')->addViolation();
}
}),
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Visitor::class,
'error_bubbling' => false,
'delete-url' => '',
]);
}
}
My "booking" (shortened) class:
/**
* #ORM\Entity(repositoryClass="App\Repository\BookingRepository")
*/
class Booking
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Visitor", mappedBy="booking", orphanRemoval=true, cascade={"persist"})
* #Assert\Valid
*/
private $visitors;
}
And finally my "visitor" (shortened) class:
/**
* #ORM\Entity(repositoryClass="App\Repository\VisitorRepository")
*/
class Visitor
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=45, nullable=true)
*/
private $phone;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $email;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Booking", inversedBy="visitors")
* #ORM\JoinColumn(nullable=false)
*/
private $booking;
/**
* #Assert\Callback
*/
public function validateAtLeastEmailOrPhone(ExecutionContextInterface $context, $payload)
{
if ($this->getPhone() === null && $this->getEmail() === null) {
$context->buildViolation('validator.visitor.email-or-phone-required-for-all')->addViolation();
}
}
}
I've been able to workaround the problem by adding a property to my VisitorType form that I define with the Callback constraint on the phone value and then check it with a Callback constraint on the email field, but it doesn't seem very "good practice".
If I only try to call the Callback constraint I get the following error message: "Warning: get_class() expects parameter 1 to be object, string given"
Any help is highly appreciated!
Instead of an callback function you could create your own Constraint. Then the Check would be reusable.
I've been using this to check the password on registration against custom rules.

Form Subscriber and "this form should not contain extra fields" error

I'm using symfony 2.3, so apparently, I can't use the 'allow_extra_fields' option discussed here.
I have a main Form Type, RegistrationStep1UserType :
/**
* Class RegistrationStep1UserType
* #package Evo\DeclarationBundle\Form\Type
*/
class RegistrationStep1UserType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('customer', new RegistrationStep1CustomerType(), [
'label' => false,
'data_class' => 'Evo\UserBundle\Entity\Customer',
'cascade_validation' => true,
])
->add('declaration', 'evo_declaration_bundle_registration_step1_declaration_type', [
'label' => false,
'cascade_validation' => true,
])
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Evo\UserBundle\Entity\User',
'validation_groups' => false,
));
}
/**
* #return string
*/
public function getName()
{
return 'evo_declaration_bundle_registration_step1_user_type';
}
}
This form type includes an embedded Form Type (on "declaration" field), RegistrationStep1DeclarationType, registered as a service :
/**
* Class RegistrationStep1DeclarationType
* #package Evo\DeclarationBundle\Form\Type
*/
class RegistrationStep1DeclarationType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var EventSubscriberInterface
*/
private $addBirthCountyFieldSubscriber;
/**
* RegistrationStep1DeclarationType constructor.
* #param EntityManagerInterface $em
* #param EventSubscriberInterface $addBirthCountyFieldSubscriber
*/
public function __construct(EntityManagerInterface $em, EventSubscriberInterface $addBirthCountyFieldSubscriber)
{
$this->em = $em;
$this->addBirthCountyFieldSubscriber = $addBirthCountyFieldSubscriber;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('birthLastname', null, [
'label' => 'Nom de naissance',
'attr' => [
'required' => true,
],
])
->add('nationality', 'entity', [
'label' => 'Nationalité',
'class' => 'Evo\GeoBundle\Entity\Country',
'property' => 'nationalityFr',
'attr' => [
'required' => true,
'class' => 'selectpicker',
],
'preferred_choices' => $this->fillPreferredNationalities(),
])
->add('birthCountry', 'entity', [
'label' => 'Pays de naissance',
'class' => 'Evo\GeoBundle\Entity\Country',
'property' => 'nameFr',
'empty_value' => '',
'empty_data' => null,
'attr' => [
'required' => true,
'class' => 'trigger-form-modification selectpicker',
],
'preferred_choices' => $this->fillPreferredBirthCountries(),
])
->add('birthCity', null, [
'label' => 'Ville de naissance',
'attr' => [
'required' => true,
],
])
;
$builder->get("birthCountry")->addEventSubscriber($this->addBirthCountyFieldSubscriber);
}
/**
* #return array
*/
private function fillPreferredNationalities()
{
$nationalities = $this->em->getRepository("EvoGeoBundle:Country")->findBy(["isDefault" => true]);
return $nationalities;
}
/**
* #return array|\Evo\GeoBundle\Entity\Country[]
*/
private function fillPreferredBirthCountries()
{
$countries = $this->em->getRepository("EvoGeoBundle:Country")->findBy(["isDefault" => true]);
return $countries;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'required' => false,
'data_class' => 'Evo\DeclarationBundle\Entity\Declaration',
'validation_groups' => false,
));
}
/**
* #return string
*/
public function getName()
{
return 'evo_declaration_bundle_registration_step1_declaration_type';
}
}
This embedded Form Type adds a Subscriber (registered as a service too, because it needs injection of EntityManager) on the "birthCountry" field.
The goal is to dynamically add or remove an extra field (called "birthCounty") depending on the value of the birthCountry choice list (note the 2 fields are different here, "birthCountry" and "birthCounty").
Here is the Subscriber :
/**
* Class AddBirthCountyFieldSubscriber
* #package Evo\CalculatorBundle\Form\EventListener
*/
class AddBirthCountyFieldSubscriber implements EventSubscriberInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* AddBirthCountyFieldSubscriber constructor.
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::POST_SET_DATA => 'postSetData',
FormEvents::POST_SUBMIT => 'postSubmit',
);
}
/**
* #param FormEvent $event
*/
public function postSetData(FormEvent $event)
{
$birthCountry = $event->getData();
$form = $event->getForm()->getParent();
$this->removeConditionalFields($form);
if ($birthCountry instanceof Country && true === $birthCountry->getIsDefault()) {
$this->addBirthCountyField($form);
}
}
/**
* #param FormEvent $event
*/
public function postSubmit(FormEvent $event)
{
$birthCountry = $event->getData();
$form = $event->getForm()->getParent();
if (!empty($birthCountry)) {
$country = $this->em->getRepository("EvoGeoBundle:Country")->find($birthCountry);
$this->removeConditionalFields($form);
if ($country instanceof Country && true === $country->getIsDefault()) {
$this->addBirthCountyField($form);
}
}
}
/**
* #param FormInterface $form
*/
private function addBirthCountyField(FormInterface $form)
{
$form
->add('birthCounty', 'evo_geo_bundle_autocomplete_county_type', [
'label' => 'Département de naissance',
'attr' => [
'required' => true,
],
])
;
}
/**
* #param FormInterface $form
*/
private function removeConditionalFields(FormInterface $form)
{
$form->remove('birthCounty');
}
}
In the view, when the "birthCountry" choice list changes, it trigger an AJAX request to the controller, which handles the request and render the view again (as explained in the documentation about dynamic form submission) :
$form = $this->createForm(new RegistrationStep1UserType(), $user);
if ($request->isXmlHttpRequest()) {
$form->handleRequest($request);
} elseif ("POST" == $request->getMethod()) {
[...]
}
The problem is the following :
When I make a change on the birthCountry choice list and select a Country supposed to hide the "birthCounty" field, the form correctly render without that field, but it shows an error message :
Ce formulaire ne doit pas contenir des champs supplémentaires.
or
this form should not contain extra fields (in english)
I tried many different solutions :
adding a 'validation_groups' option to RegistrationStep1UserType and RegistrationStep1DeclarationType
adding a preSubmit event to AddBirthCountyFieldSubscriber replicating the logic of preSetData and postSubmit methods
even adding 'mapped' => false, to the birthCounty field triggers the error. very surprising
Even $form->getExtraData() is empty if I dump it just after $form->handleRequest($request);
But in vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Validator\Constraints\FormValidator, I can see an extra field
array(1) {
["birthCounty"]=>
string(0) ""
}
here :
// Mark the form with an error if it contains extra fields
if (count($form->getExtraData()) > 0) {
echo '<pre>';
\Doctrine\Common\Util\Debug::dump($form->getExtraData());
echo '</pre>';
die();
$this->context->addViolation(
$config->getOption('extra_fields_message'),
array('{{ extra_fields }}' => implode('", "', array_keys($form->getExtraData()))),
$form->getExtraData()
);
}
Did I miss something about form dynamic extra fields ?
I did not analyzed all the question but, I guess, that you can invert the logic: always add that field and remove it when the condition is not satisfied.
That way you don't need to perform operations in postSubmit (that is where the issue is)

Symfony CollectionType with "allow_add" not adding child entity to parent entity

On Symfony 2.8, I got the following entities:
Contact:
class Contact
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\Column
* #Assert\NotBlank
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="EmailContact", mappedBy="contact", cascade={"persist"})
* #Assert\Valid
*/
protected $emails;
// ...
/**
* Add emails
*
* #param \AppBundle\Entity\EmailContact $emails
* #return Contact
*/
public function addEmail(\AppBundle\Entity\EmailContact $emails)
{
$this->emails[] = $emails;
$emails->setContact($this); //this line added by me
return $this;
}
// ...
EmailContact:
class EmailContact
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\Column
* #Assert\NotBlank
*/
protected $email;
/**
* #ORM\ManyToOne(targetEntity="Contact", inversedBy="emails")
* #ORM\JoinColumn(nullable=false)
*/
protected $contact;
// ...
The rest of the methods were automatically generated by the doctrine:generate:entities command.
My forms are as follows:
ContactType:
class ContactType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, [
'label' => 'contact.name',
])
->add('emails', CollectionType::class, [
'label' => false,
'entry_options' => array('label' => false),
'entry_type' => EmailContactType::class,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'prototype' => true,
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Contact'
]);
}
EmailContactType:
class EmailContactType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class, [
'label' => 'emailContact.email',
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\EmailContact'
]);
}
I do the javascript to add extra fields to the request, and submit it. Example request (from Symfony Profiler):
[
name => test4,
emails => [
0 => [
email => t#t.t4
],
1 => [
email => t#t.t5
]
],
_token => ...
]
But I get the following error:
An exception occurred while executing 'INSERT INTO email_contact ...
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'contact_id' cannot be null
Debugging, I see that the addEmail method above never gets called. What is happening here?
You missed by_reference => false in form collection definition
->add('emails', CollectionType::class, [
'label' => false,
'entry_options' => array('label' => false),
'entry_type' => EmailContactType::class,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'prototype' => true,
'by_reference' => false; // <--- you missed this
]);
Take a look here
Your code should run as expected after this modification.
Moreover remember that if you have a setEmails() method inside Contact class, the framework end up to calling it and so you need (for each element of the collection) to set contact as well (as you're correctly doing in addEmails())

zend2 insert into collection/database

I have some problem with colection in zf2 and doctryne.
Problem: I add some client and need have some multiselect.
ClientEntity
/**
* #ORM\MappedSuperclass
*/
class Client {
...
/**
* #ORM\ManyToOne(targetEntity="\Module\Model\Job")
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
*/
protected $job;
/**
* #ORM\OneToMany(targetEntity="SomeOption", mappedBy="job", cascade= {"persist", "remove"})
* */
protected $someOption;
...
public function __construct() {
$this->someOption= new ArrayCollection();
}
OptionEntity
/**
* #ORM\MappedSuperclass
*/
class SomeOption{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="\Module\Model\Client")
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
* */
protected $job;
/**
* #ORM\Column(type="string", nullable=false)
*/
protected $option;
}
Bought model have getter and setter, in Client model have:
public function addSomeOption(Collection $options) {
foreach ($options as $option) {
$option->setJob($this);
$this->someOption->add($option);
}
return $this;
}
public function removeSomeOption(Collection $options) {
foreach ($options as $option) {
$option->setJob(null);
$this->someOption->removeElement($option);
}
return $this;
}
public function getSomeOption() {
return $this->someOption;
}
Form:
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'someOption',
'attributes' => array(
'id' => 'someOption',
),
'options' => array(
'label' => 'Rozliczenie',
'value_options' => array(
array('value' => '1r', 'label' => '1/rok'),
array('value' => '1m', 'label' => '1/miesiąc'),
array('value' => '1w', 'label' => '1/tydzień'),
array('value' => '1d', 'label' => '1/dzień'),
),
),
'attributes' => array(
'class' => 'span12 settlement chosen',
'multiple' => 'multiple'
)
));
after this i need have 1 row of client and 1+ row of someOption, may any help to repair code ? or much more will be explain what i make wrong.

How to merge 2 form in Symfony2

I'm trying to create a very simple forum with Symfony2.
My entities are:
ForumCategory (name, description...)
ForumTopic (category_id, title)
ForumPost (isFirstPost, body, topic_id, author_id...)
When a user try to create a Topic, I want to display only one form in the same page to create a Topic and the first post message. Like:
Insert topic title: ...
Insert topic body (related Post Body): ...
[...]
How can I do that? It's possible merge two form in this case?
Make a form type that contains both of your sub forms.
class MergedFormType
$builder->add('topic', new TopicFormType());
$builder->add('post', new PostFormType());
In your controller just pass an array to MergedFormType
public function myAction()
$formData['topic'] = $topic;
$formData['post'] = $post;
$form = $this->createForm(new MergedFormType(), $formData);
Incase if you are looking to merge forms for 2 entities with one to many or one to one relationship; you will need to use form collection extension of symfony 2 component. for eg: where Task entity has many Tags
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array('type' => new TagType()));
}
Rendering can be done this way
{{ form_start(form) }}
<h3>Tags</h3>
<ul class="tags">
{# iterate over each existing tag and render its only field: name #}
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
Further details:
http://symfony.com/doc/2.7/cookbook/form/form_collections.html
You can also map the same entity to multiple merged forms.
$entity = new Form();
$form = $this->get('form.factory')->create(FormType::class, [
'form_builder' => $entity,
'submit_builder' => $entity,
]);
FormType.php
<?php
namespace GenyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\OptionsResolver\OptionsResolver;
use GenyBundle\Entity\Form;
class FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('form_builder', FormBuilderType::class, [
'data_class' => Form::class,
'label' => false, // Important!
])
->add('submit_builder', SubmitBuilderType::class, [
'data_class' => Form::class,
'label' => false,
])
->add('save', Type\SubmitType::class, [
'label' => 'geny.type.form.save.label',
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'translation_domain' => 'geny',
]);
}
}
FormBuilderType.php
<?php
namespace GenyBundle\Form\Type;
use GenyBundle\Base\BaseType;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FormBuilderType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', Type\TextType::class, [
'attr' => [
'placeholder' => 'geny.type.form.title.placeholder',
],
'empty_data' => $this->get('translator')->trans('geny.type.form.title.default', [], 'geny'),
'label' => 'geny.type.form.title.label',
'required' => true,
])
->add('description', Type\TextareaType::class, [
'attr' => [
'placeholder' => 'geny.type.form.description.placeholder',
],
'empty_data' => null,
'label' => 'geny.type.form.description.label',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'GenyBundle\Entity\Form',
'translation_domain' => 'geny',
]);
}
}
SubmitBuilderType.php
<?php
namespace GenyBundle\Form\Type;
use GenyBundle\Base\BaseType;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SubmitBuilderType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('submit', Type\TextType::class, [
'attr' => [
'placeholder' => 'geny.type.submit.submit.placeholder',
],
'empty_data' => $this->get('translator')->trans('geny.type.submit.submit.default', [], 'geny'),
'label' => 'geny.type.submit.submit.label',
'required' => true,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'GenyBundle\Entity\Form',
'translation_domain' => 'geny',
]);
}
}
Form.php
<?php
namespace GenyBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Table(name="geny_form")
* #ORM\Entity(repositoryClass="GenyBundle\Repository\FormRepository")
* #ORM\ChangeTrackingPolicy("DEFERRED_EXPLICIT")
* #Serializer\ExclusionPolicy("NONE")
*/
class Form
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Id
* #Serializer\Exclude
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=128)
* #Assert\Length(min = 1, max = 128)
* #Serializer\Type("string")
*/
protected $title;
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=true)
* #Assert\Length(min = 0, max = 4096)
* #Serializer\Type("string")
*/
protected $description;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Field", mappedBy="form", cascade={"all"}, orphanRemoval=true)
* #ORM\OrderBy({"position" = "ASC"})
* #Assert\Valid()
* #Serializer\Type("ArrayCollection<GenyBundle\Entity\Field>")
*/
protected $fields;
/**
* #var string
*
* #ORM\Column(name="submit", type="text")
* #Assert\Length(min = 1, max = 64)
* #Serializer\Type("string")
*/
protected $submit;
}
Result: