I want to enable select 2 search in my Symfony form what i tried so far:
In my form class i have this:
->add('parent', EntityType::class, [
'class' => Category::class,
'choice_label' => 'title',
'attr' => [
'class' => 'select2'
]
])
In my twig file this :
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2-rc.1/css/select2.min.css" rel="stylesheet" />
<!-- Loading jquery here--><script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2-rc.1/js/select2.min.js"></script>
</head>
{{ form_start(form) }}
<script type="text/javascript">
$('select').select2();
</script>
{{ form_widget(form) }}
{{ form_end(form) }}
But i do not get the dropdown with the search bar. Just the default dropdown menu of Symfony. What am I doing wrong
The main reason is that the field is created after you try and target it, by this line:
{{ form_widget(form) }}
The JavaScript must be executed after that in order to be able to target the field (besides, the HTML structure of your template is wrong).
Try this :
<!DOCTYPE html>
<html>
<head>
<title>Test form</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2-rc.1/css/select2.min.css" rel="stylesheet" />
<!-- Loading jquery here--><script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2-rc.1/js/select2.min.js"></script>
</head>
<body>
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
<script>
$('select').select2();
</script>
</body>
</html>
It's usually better to wait for the page to be loaded before executing scripts, using jQuery you could ensure that it's the case by changing the script to this:
<script>
$(document).ready(function(){
$('.select2').select2();
});
</script>
Notice that I also changed the jQuery selector to use the class you've added to the field in your form builder. This way you control the select field you want to target.
You are initiating the select2 components without configuration, so it doesn't know where is the data source.
Before start coding, you need to install and configure FOSJsRoutingBundle. This bundle will help you with access to ajax routes
For fully configured sync symfony-forms~select2 you could do something like this.
Entity Person
class Person
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="SEQUENCE")
* #ORM\SequenceGenerator(sequenceName="person_id_seq", allocationSize=1, initialValue=1)
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", nullable=true)
*/
private $name;
/**
* #var Country
*
* #ORM\ManyToOne(targetEntity="Country")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="country_id", referencedColumnName="id")
* })
*/
private $country;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Person
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set country
*
* #param \AppBundle\Entity\Country $country
*
* #return Person
*/
public function setCountry(\AppBundle\Entity\Country $country = null)
{
$this->country = $country;
return $this;
}
/**
* Get country
*
* #return \AppBundle\Entity\Country
*/
public function getCountry()
{
return $this->country;
}
public function __toString()
{
return $this->name;
}
}
Entity Country
class Country
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="SEQUENCE")
* #ORM\SequenceGenerator(sequenceName="country_id_seq", allocationSize=1, initialValue=1)
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", nullable=true)
*/
private $name;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Country
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
public function __toString()
{
return $this->name;
}
}
Country repository
class CountryRepository extends \Doctrine\ORM\EntityRepository
{
public function countriesSelect2($term)
{
$qb = $this->createQueryBuilder('c');
$qb->where(
$qb->expr()->like($qb->expr()->lower('c.name'), ':term')
)
->setParameter('term', '%' . strtolower($term) . '%');
return $qb->getQuery()->getArrayResult();
}
}
Country controller
Check how the route is exposed to the options parameter and returns a JsonResponse. You could also use a serializer too.
/**
* Country controller.
*
* #Route("countries")
*/
class CountryController extends Controller
{
/**
* Lists all person entities.
*
* #Route("/", name="countries",options={"expose"=true})
* #Method("GET")
*/
public function indexAction(Request $request)
{
$countryRepo = $this->getDoctrine()->getRepository('AppBundle:Country');
$data = $countryRepo->countriesSelect2($request->get('q', ''));
//$response = $this->get('serializer')->serialize($data,'json');
return new JsonResponse($data);
}
}
So far so good, now comes the good parts, let's go and configure our form
PersonType
class PersonType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('country',EntityType::class,[
'class' => Country::class,
'attr' => [
'class' => 'select2', // the class to use with jquery
'data-source' => 'countries', //the exposed route name for data-soirce as attr
'data-allow-clear' => 'true'//another extra attr to customize
],
]);
}/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_person';
}
}
JS, showing the select2
Remember, you have configured already the select2 options with attributes, you just have to use them properly
$(document).ready(function () {
$('.select2').each(function () {//using the select2 class
if (!$().select2) {//checking the script
return;
}
$.fn.select2.defaults.set("theme", "bootstrap");//some theming if you want
$($(this)).select2({
placeholder: "Select",
width: 'auto',
allowClear: $(this).attr("data-allow-clear") ? $(this).attr("data-allow-clear") : true, //using my options from the form
ajax: {
url: Routing.generate($(this).attr("data-source")), //here its the magic
dataType: 'json',
processResults: function (data) {
//console.log(data);
return {
results: $.map(data, function (item) {
return {
text: item.name, //you need to map this because the plugin accepts only id and text
id: item.id
}
})
};
}
}
});
});
});
after that, all is done. All the code is working as I tested my self
Hope it helps!
There is a nice bundle for it: TetranzBundle
You can configure your form field in FormType class like that:
->add('product', Select2EntityType::class, [
'label'=>'product',
'required'=>true,
'mapped'=>true,
'multiple' => false,
'remote_route' => 'product_select2_ajax',
'class' => 'AppBundle:Product',
// 'property' => 'name',
'minimum_input_length' => 0,
'page_limit' => 10,
'allow_clear' => true,
'delay' => 250,
'cache' => true,
'cache_timeout' => 60000, // if 'cache' is true
'language' => 'pl',
'placeholder' => "select.product",
])
Try this :
<script type="text/javascript">
$(document).ready(function() {
$('.select2').select2(); //instead $('select2').select2();
});
</script>
see How to select a class in Jquery and the basic usage exemple
Related
Hello I am using Symfony 4.
I have managed to link up to two select box with form events, but I need to have three dynamic select box.
This is the relation between my entities:
Country -> Province -> City.
These are linked to a Person entity like this
When I add a new person I should be able to select a Country and have the Province dropdown updated in accordance to Country selection; same thing for the City dropdown after I have selected a Province.
I have made things working for Country and Province following the official Symfony guide here
https://symfony.com/doc/current/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms
How should I manage adding the third dropdown?
This is my Country entity:
<?php
namespace App\Entity\Geo;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\Geo\CountryRepository")
*/
class Country
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank()
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Geo\Province", mappedBy="country")
* #ORM\JoinColumn(nullable=false)
*/
private $provinces;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Geo\City", mappedBy="country")
* #ORM\JoinColumn(nullable=false)
*/
private $cities;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Geo\Person", mappedBy="country")
* #ORM\JoinColumn(nullable=false)
*/
private $persons;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getProvinces()
{
return $this->provinces;
}
/**
* #param mixed $provinces
*/
public function setProvinces($provinces): void
{
$this->provinces = $provinces;
}
/**
* #return mixed
*/
public function getCities()
{
return $this->cities;
}
/**
* #param mixed $cities
*/
public function setCities($cities): void
{
$this->cities = $cities;
}
/**
* #return mixed
*/
public function getPersons()
{
return $this->persons;
}
/**
* #param mixed $persons
*/
public function setPersons($persons): void
{
$this->persons = $persons;
}
}
This is my Province entity:
<?php
namespace App\Entity\Geo;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\Geo\ProvinceRepository")
*/
class Province
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank()
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Geo\Country", inversedBy="provinces")
* #ORM\JoinColumn(nullable=false)
*/
private $country;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Geo\City", mappedBy="province")
* #ORM\JoinColumn(nullable=false)
*/
private $cities;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Geo\Person", mappedBy="province")
* #ORM\JoinColumn(nullable=false)
*/
private $persons;
public function __toString() {
return $this->name;
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getCountry()
{
return $this->country;
}
/**
* #param mixed $country
*/
public function setCountry($country): void
{
$this->country = $country;
}
/**
* #return mixed
*/
public function getCities()
{
return $this->cities;
}
/**
* #param mixed $cities
*/
public function setCities($cities): void
{
$this->cities = $cities;
}
/**
* #return mixed
*/
public function getPersons()
{
return $this->persons;
}
/**
* #param mixed $persons
*/
public function setPersons($persons): void
{
$this->persons = $persons;
}
}
This is my City entity:
<?php
namespace App\Entity\Geo;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\Geo\CityRepository")
*/
class City
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank()
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Geo\Province", inversedBy="cities")
* #ORM\JoinColumn(nullable=false)
*/
private $province;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Geo\Country", inversedBy="cities")
* #ORM\JoinColumn(nullable=false)
*/
private $country;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Geo\Person", mappedBy="city")
* #ORM\JoinColumn(nullable=false)
*/
private $persons;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getProvince()
{
return $this->province;
}
/**
* #param mixed $province
*/
public function setProvince($province): void
{
$this->province = $province;
}
/**
* #return mixed
*/
public function getCountry()
{
return $this->country;
}
/**
* #param mixed $country
*/
public function setCountry($country): void
{
$this->country = $country;
}
/**
* #return mixed
*/
public function getPersons()
{
return $this->persons;
}
/**
* #param mixed $persons
*/
public function setPersons($persons): void
{
$this->persons = $persons;
}
}
This is my form to add a Person (PersonType.php)
<?php
namespace App\Form\Geo;
use App\Entity\Geo\Person;
use App\Entity\Geo\Country;
use App\Entity\Geo\Province;
use App\Entity\Geo\City;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, ['label' => "Name"])
->add('country', EntityType::class, [
'class' => Country::class,
'choice_label' => function(Country $country) {
return $country->getName();
},
'placeholder' => 'Choose a Country'
])
;
$formModifier = function (FormInterface $form, Country $country = null) {
$provinces = null === $country ? [] : $country->getProvinces();
$form->add('province', EntityType::class, [
'class' => Province::class,
'placeholder' => 'Choose a Province',
'choices' => $provinces,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getCountry());
}
);
$builder->get('country')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$country = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $country);
}
);
$builder->add( 'save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' =>Person::class
]);
}
}
This is my twig template (person-add.html.twig)
{% extends 'base.html.twig' %}
{% block title %}Add Person{% endblock %}
{% block body %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.country) }}
{{ form_row(form.province) }}
{{ form_end(form) }}
<script>
$(document).ready(function() {
var $country = $('#person_country');
// When sport gets selected ...
$country.change(function () {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected sport value.
var data = {};
data[$country.attr('name')] = $country.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
// Replace current position field ...
$('#person_province').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#person_province')
);
// Position field now displays the appropriate positions.
}
});
})
});
</script>
{% endblock %}
Thanks to this post I have managed to change my PersonType.php form file like this:
<?php
namespace App\Form\Geo;
use App\Entity\Geo\Person;
use App\Entity\Geo\Country;
use App\Entity\Geo\Province;
use App\Entity\Geo\City;
use App\Repository\Geo\CityRepository;
use App\Repository\Geo\ProvinceRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
//
->add('country', EntityType::class, [
'class' => Country::class,
'label' => 'Country',
'required' => true,
'choice_label' => function(Country $country) {
return $country->getName();
},
'invalid_message' => 'You must select a Country',
'placeholder' => 'Select Country',
]);
//**************** Start Province Form
$addProvinceForm = function (FormInterface $form, $country_id) {
// it would be easier to use a Park entity here,
// but it's not trivial to get it in the PRE_SUBMIT events
$form->add('province', EntityType::class, [
'class' => Province::class,
'label' => 'Province',
'required' => true,
'invalid_message' => 'Choose a Province',
'placeholder' => null === $country_id ? 'Choose a Country first' : 'Select Province',
'query_builder' => function (ProvinceRepository $repository) use ($country_id) {
return $repository->createQueryBuilder('p')
->innerJoin('p.country', 'c')
->where('c.id = :country')
->setParameter('country', $country_id)
;
}
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($addProvinceForm) {
$country = $event->getData()->getCountry();
$country_id = $country ? $country->getId() : null;
$addProvinceForm($event->getForm(), $country_id);
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($addProvinceForm) {
$data = $event->getData();
$country_id = array_key_exists('country', $data) ? $data['country'] : null;
$addProvinceForm($event->getForm(), $country_id);
}
);
//**************** End Province Form
//**************** Start City Form
$addCityForm = function (FormInterface $form, $province_id) {
$form->add('city', EntityType::class, [
'class' => City::class,
'label' => 'City',
'required' => true,
'invalid_message' => 'You must choose a City',
'placeholder' => null === $province_id ? 'Choose a Province first' : 'Choose a City',
'query_builder' => function (CityRepository $repository) use ($province_id) {
return $repository->createQueryBuilder('ci')
->innerJoin('ci.province', 'pr')
->where('pr.id = :province')
->setParameter('province', $province_id)
;
}
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($addCityForm) {
$province = $event->getData()->getProvince();
$province_id = $province ? $province->getId() : null;
$addCityForm($event->getForm(), $province_id);
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($addCityForm) {
$data = $event->getData();
$province_id = array_key_exists('province', $data) ? $data['province'] : null;
$addCityForm($event->getForm(), $province_id);
}
);
//**************** End City Form
$builder->add( 'save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' =>Person::class
]);
}
}
The Province dropdown works as expected when you first select a Country.
The problem is the City dropdown: nothing changes after you select a Province.
If everything is ok with the query executed inside the PersonType.php file, I think I am doing something wrong with the javascript. Here's my code:
<script>
$(document).ready(function() {
var $country = $('#person_country');
var $province = $('#person_province');
// When country gets selected ...
$country.change(function () {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected country value.
var data = {};
data[$country.attr('name')] = $country.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
// Replace current province field ...
$('#person_province').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#person_province')
);
}
});
});
// When province gets selected ...
$province.change( function () {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected province value.
var data = {};
data[$province.attr('name')] = $province.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
// Replace current city field ...
$('#person_city').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#person_city')
);
}
});
});
});
</script>
I'am building a simple product page. Some products could have some extra options (productOptions). These options could be selected due to selectboxes. But the problem is as follows:
I've the following Entities:
Product
#src/appbundle/entity
<?php
/**
* Product
*/
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
* #ORM\Table(name="product")
*/
class Product {
/**
* #var string id
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string title
* #ORM\Column(type="string")
*/
protected $title;
/**
* #var ArrayCollection|ProductOption[]
* #ORM\OneToMany(targetEntity="AppBundle\Entity\ProductOption", mappedBy="product", fetch="EAGER")
*/
protected $productOptions;
protected $tempProductOptions;
/**
* #return string
*/
public function getId() {
return $this->id;
}
/**
* #return string
*/
public function getTitle() {
return $this->title;
}
/**
* #param string $title
*/
public function setTitle($title) {
$this->title = $title;
}
public function __construct() {
$this->productOptions = new ArrayCollection();
}
public function addProductOption(ProductOption $productOption){
if($this->productOptions->contains($productOption)){
return;
}
$productOption->setProduct($this);
$this->productOptions->add($productOption);
}
public function removeProductOption(ProductOption $productOption){
if(!$this->productOptions->contains($productOption)){
return;
}
$productOption->setProduct(null);
$this->productOptions->removeElement($productOption);
}
/**
* #return ProductOption[]|ArrayCollection
*/
public function getProductOptions() {
return $this->productOptions->toArray();
}
}
ProductOption
#src/appbundle/entity
<?php
/**
* ProductOption
*/
namespace AppBundle\Entity;
use AppBundle\Repository\GenusRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProductOptionRepository")
* #ORM\Table(name="product_option")
*/
class ProductOption {
/**
* #var int id
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string title
* #ORM\Column(type="string")
*/
protected $title;
/**
* #var float price
* #ORM\Column(type="float", scale=2)
*/
protected $price;
/**
* ProductOption connected to specific product
* #ORM\ManyToOne(targetEntity="Product", inversedBy="productOptions")
* #ORM\JoinColumn(nullable=false)
*/
protected $product;
/**
* #return int
*/
public function getId() {
return $this->id;
}
/**
* #param int $id
*/
public function setId($id) {
$this->id = $id;
}
/**
* #return string
*/
public function getTitle() {
return $this->title;
}
/**
* #param string $title
*/
public function setTitle($title) {
$this->title = $title;
}
/**
* #return float
*/
public function getPrice() {
return $this->price;
}
/**
* #param float $price
*/
public function setPrice($price) {
$this->price = $price;
}
/**
* #return Product
*/
public function getProduct() {
return $this->product;
}
/**
* #param Product $product
*/
public function setProduct($product) {
$this->product = $product;
}
public function __toString() {
return (string)$this->getTitle() . '(€'.$this->getPrice().')';
}
}
This is my ProductType (formbuilder)
#src/appbundle/form
<?php
namespace AppBundle\Form;
use AppBundle\Entity\Product;
use AppBundle\Entity\ProductOption;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('title', TextType::class, [
'label' => 'titel'
]);
$builder->add('tempProductOptions', EntityType::class, [
'class' => ProductOption::class,
'multiple' => true,
'expanded' => true,
'mapped' => false,
'choice_attr' => function($productOption, $key, $index) {
/** #var ProductOption $productOption */
return [
'data-price' => $productOption->getPrice(),
];
},
]);
$builder->add('save', SubmitType::class, array(
'label' => 'Opslaan'
));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => Product::class,
));
}
public function getBlockPrefix() {
return 'product';
}
}
Twig file
its a very basic example
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>test product page</h1>
{{ dump(form) }}
{{ form_start(form) }}
{{ form_row(form.titel) }}
{{ form_row(form.productOptions) }}
{{ form_row(form.save) }}
{{ form_end(form) }}
</body>
</html>
For exmaple product 1 has two options:
option 1 (50 euro)
option 2 (60 euro)
In the database option 1 and 2 are connected to product 1
table product_option
ID title price product_id
1 option 1 50 1
2 option 2 60 1
But in the view the options are already checked (because they connected through db). Is it possible to uncheck the checkboxes.
Short version. Each product could have extra options. These options are stored in db. When creating the form on product page the user could choose for these options, but they are already checked.
Also added the following to my formbuilder
public function finishView(FormView $view, FormInterface $form, array $options) {
foreach ($view->children['productOptions']->children as $productOption) {
$productOption->vars['checked'] = false;
}
}
but the checkboxes are still being checked
I've found an solution:
public function buildForm(FormBuilderInterface $builder, array $options) {
// arraycollection with productOptions
$productOptions = $options['data']->getProductOptions();
$builder->add('id', HiddenType::class, ['mapped' => false]);
$builder->add('productOptions', ChoiceType::class, [
'choices' => $productOptions,
'expanded' => true,
'multiple' => true,
'mapped' => false,
'choice_value' => function (ProductOption $productOption) {
return $productOption->getId();
},
'choice_label' => function(ProductOption $productOption, $key, $index) {
return $productOption->__toString();
},
'choice_attr' => function(ProductOption $productOption, $key, $index) {
return [
'data-prijs' => $productOption->getPrijs(),
];
},
]);
// rest here ...
How can I successfully embed a running image upload form inside a classified form with OneToOne relationship ?
first post ever on Stackoverflow so please be gentle (and I'm french ...^^)
I have a Symfony 3.3 project, running great. I use VichUploaderBundle for upload and LiipImagine for rendering. Currently, NO PROBLEM for a single image File upload !
I have an uploadtest page, I show one image file upload form, I upload the file ... it goes throught the controller and everything is fine. File uploaded and moved in directory, page shows new image, with all filters. Great !
My real problem comes when embedding the form inside another form.
I would like to embed my "image form" inside a classified form. I started by creating the field mainImage inside this classified form. The relation from the classified is OneToOne, and of course it has cascade="persist" & "remove".
But on validation, nothing is triggered and I got the following error on the image_name :
--- An exception occurred while executing 'INSERT INTO vehicle_ad (createdAt, updatedAt, published, publishedAt, friendsPriority,
friendsPriorityStart, purchaseDate, sellPrice, currency, odometerType,
currentOdometer, energy, gearbox, mainColor, nickname, description,
creator_id, vehicle_brand_id, vehicle_model_id, main_image_id,
vehicle_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?)' with params ["2018-01-23 17:15:39", "2018-01-23
17:15:39", 0, null, 0, null, null, 2650, "eur", "kilometers", 17340,
null, null, null, null, null, 1, 3, 1, null, null]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Le champ
'main_image_id' ne peut être vide (null)
File is empty, vichuploader namer not triggered ... and I am stuck with it since a few days. I did a lot of research ... so any help will be greatly appreciated.
I try to give my code as clearly as possible. First, what is OK (single image upload) :
Image class
<?php
namespace Gac\MediasBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Image
*
* #ORM\Table(name="image")
* #ORM\Entity(repositoryClass="Gac\MediasBundle\Repository\ImageRepository")
* #Vich\Uploadable
*/
class Image
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/* ----------------------------------------------------------------------
--------- Connection with other entities
------------------------------------------------------------------------- */
/**
* #ORM\ManyToOne(targetEntity="Gac\UserBundle\Entity\User", inversedBy="images")
* #ORM\JoinColumn(nullable=false)
*/
private $creator;
/**
* #ORM\ManyToOne(targetEntity="Gac\AdsBundle\Entity\VehicleAd", inversedBy="images")
* #ORM\JoinColumn(nullable=true)
*/
private $vehicleAd;
// -------------------------------------------------------------------------------
/**
* #var string
*
* #ORM\Column(name="imageName", type="string", length=255)
*/
private $imageName;
/**
* #Vich\UploadableField(mapping="users_images", fileNameProperty="imageName")
* #var File
*/
private $imageFile;
/**
* #var \DateTime
*
* #ORM\Column(name="createdAt", type="datetime")
* #Assert\DateTime()
*/
private $createdAt;
/**
* #var \DateTime
*
* #ORM\Column(name="updatedAt", type="datetime")
* #Assert\DateTime()
*/
private $updatedAt;
/**
* #var \DateTime
*
* #ORM\Column(name="hiddenAt", type="datetime", nullable=true)
* #Assert\DateTime()
*/
private $hiddenAt;
public function __construct($thisuser)
{
$this->creator = $thisuser;
$this->createdAt = new \Datetime();
$this->updatedAt = new \Datetime();
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set creator
*
* #param \Gac\UserBundle\Entity\User $creator
*
* #return Image
*/
public function setCreator(\Gac\UserBundle\Entity\User $creator)
{
$this->creator = $creator;
return $this;
}
/**
* Get creator
*
* #return \Gac\UserBundle\Entity\User
*/
public function getCreator()
{
return $this->creator;
}
/**
* Set imageName
*
* #param string $imageName
*
* #return Image
*/
public function setImageName($imageName)
{
$this->imageName = $imageName;
return $this;
}
/**
* Get imageName
*
* #return string
*/
public function getImageName()
{
return $this->imageName;
}
public function setImageFile(File $image = null)
{
$this->imageFile = $image;
// VERY IMPORTANT:
// It is required that at least one field changes if you are using Doctrine,
// otherwise the event listeners won't be called and the file is lost
if ($image) {
$this->updatedAt = new \DateTime();
}
}
/**
* Get imageFile
*/
public function getImageFile()
{
return $this->imageFile;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return Image
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set updatedAt
*
* #param \DateTime $updatedAt
*
* #return Image
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Set hiddenAt
*
* #param \DateTime $hiddenAt
*
* #return Image
*/
public function setHiddenAt($hiddenAt)
{
$this->hiddenAt = $hiddenAt;
return $this;
}
/**
* Get hiddenAt
*
* #return \DateTime
*/
public function getHiddenAt()
{
return $this->hiddenAt;
}
/**
* Set vehicleAd
*
* #param \Gac\AdsBundle\Entity\VehicleAd $vehicleAd
*
* #return Image
*/
public function setVehicleAd(\Gac\AdsBundle\Entity\VehicleAd $vehicleAd = null)
{
$this->vehicleAd = $vehicleAd;
return $this;
}
/**
* Get vehicleAd
*
* #return \Gac\AdsBundle\Entity\VehicleAd
*/
public function getVehicleAd()
{
return $this->vehicleAd;
}
}
Then the related IMAGE FORM :
<?php
namespace Gac\MediasBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;
class ImageType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('imageFile', VichImageType::class, [
'label' => false,
'required' => false,
'allow_delete' => false,
]);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Gac\MediasBundle\Entity\Image'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
//return 'gac_mediasbundle_image';
return 'ImageType';
}
}
And the controller
{% extends 'medias/layout.html.twig' %}
{% block bundle_content %}
<div class="jumbotron text-center">
<h1>Testing user's images UPLOAD</h1>
</div>
<p>-----------------</p>
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form) }}
<button type="submit">Upload</button>
{{ form_end(form) }}
<p>-----------------</p>
{% for picture in pictures %}
<div class="row">
<div class="media">
<div class="media-left media-middle">
<img class="media-object" src="{{ picture.imageName | imagine_filter('tiny_test') }}" alt="{{ 'GearsAndCoffee-'~picture.imageName }}" /><br/>
</div>
<div class="media-body">
<h4 class="media-heading">Filter name :</h4>
<em>tiny_test</em>
</div>
</div>
<div class="media">
<div class="media-left media-middle">
<a href="{{ asset('uploads/images/'~picture.imageName) }}" />
<img class="media-object" src="{{ picture.imageName | imagine_filter('std_display') }}" alt="{{ 'GearsAndCoffee-'~picture.imageName }}" /><br/>
</a>
</div>
<div class="media-body">
<h4 class="media-heading">Filter name :</h4>
<em>std_display</em>
</div>
</div>
<p>
<strong>UPDATE IMAGE</strong>
</p>
<p>
<strong>DELETE IMAGE</strong>
</p>
</div>
<p>-----------------</p>
{% endfor %}
{% endblock bundle_content %}
As an information, the VichUploader Bundle config :
vich_uploader:
db_driver: orm
#templating: true
#twig: true
#form: true
mappings:
users_images:
uri_prefix: '%app.path.users_images%'
upload_destination: '%kernel.root_dir%/../web/uploads/images'
namer: vich_uploader.namer_uniqid
inject_on_load: true
delete_on_update: true
delete_on_remove: true
And now the code of what is NOT RUNNING on my app :
The classified class
<?php
namespace Gac\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
/**
* VehicleAd
*
* #ORM\Table(name="vehicle_ad")
* #ORM\Entity(repositoryClass="Gac\AdsBundle\Repository\VehicleAdRepository")
* #ORM\HasLifecycleCallbacks
*/
class VehicleAd
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/* ----------------------------------------------------------------------
--------- Connection with other entities
------------------------------------------------------------------------- */
/**
* #ORM\ManyToOne(targetEntity="Gac\UserBundle\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $creator;
/**
* #ORM\ManyToOne(targetEntity="Gac\VehiclesBundle\Entity\VehicleBrand")
* #ORM\JoinColumn(nullable=false)
*/
private $vehicleBrand;
/**
* #ORM\ManyToOne(targetEntity="Gac\VehiclesBundle\Entity\VehicleModel")
* #ORM\JoinColumn(nullable=false)
*/
private $vehicleModel;
/**
* #ORM\OneToOne(targetEntity="Gac\MediasBundle\Entity\Image", cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false)
*/
private $mainImage;
// -------------------------------------------------------------------------------
// ---------------- General status and dates for the AD -------------
/**
* #var \DateTime
*
* #ORM\Column(name="createdAt", type="datetime")
* #Assert\DateTime()
*/
private $createdAt;
/**
* #var \DateTime
*
* #ORM\Column(name="updatedAt", type="datetime")
* #Assert\DateTime()
*/
private $updatedAt;
// ---------------------- AD main content -----------------------
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=true)
* #Assert\Type("string")
*/
private $description;
// ************************* FUNCTIONS ***************************
public function __construct($thisuser)
{
$this->creator = $thisuser;
$this->createdAt = new \Datetime();
$this->updatedAt = new \Datetime();
}
/**
* Change updatedAt field just before ORM update
*
* #ORM\PreUpdate
*/
public function changeUpdatedAt()
{
$this->updatedAt = new \Datetime();
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return VehicleAd
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set updatedAt
*
* #param \DateTime $updatedAt
*
* #return VehicleAd
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Set creator
*
* #param \Gac\UserBundle\Entity\User $creator
*
* #return VehicleAd
*/
public function setCreator(\Gac\UserBundle\Entity\User $creator)
{
$this->creator = $creator;
return $this;
}
/**
* Get creator
*
* #return \Gac\UserBundle\Entity\User
*/
public function getCreator()
{
return $this->creator;
}
/**
* Set description
*
* #param string $description
*
* #return VehicleAd
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set vehicleBrand
*
* #param \Gac\VehiclesBundle\Entity\VehicleBrand $vehicleBrand
*
* #return VehicleAd
*/
public function setVehicleBrand(\Gac\VehiclesBundle\Entity\VehicleBrand $vehicleBrand)
{
$this->vehicleBrand = $vehicleBrand;
return $this;
}
/**
* Get vehicleBrand
*
* #return \Gac\VehiclesBundle\Entity\VehicleBrand
*/
public function getVehicleBrand()
{
return $this->vehicleBrand;
}
/**
* Set vehicleModel
*
* #param \Gac\VehiclesBundle\Entity\VehicleModel $vehicleModel
*
* #return VehicleAd
*/
public function setVehicleModel(\Gac\VehiclesBundle\Entity\VehicleModel $vehicleModel)
{
$linkedBrand = $vehicleModel->getModelBrand();
$this->vehicleBrand = $linkedBrand;
$this->vehicleModel = $vehicleModel;
return $this;
}
/**
* Get vehicleModel
*
* #return \Gac\VehiclesBundle\Entity\VehicleModel
*/
public function getVehicleModel()
{
return $this->vehicleModel;
}
/**
* Set mainImage
*
* #param \Gac\MediasBundle\Entity\Image $mainImage
*
* #return VehicleAd
*/
public function setMainImage(\Gac\MediasBundle\Entity\Image $mainImage)
{
$this->mainImage = $mainImage;
return $this;
}
/**
* Get mainImage
*
* #return \Gac\MediasBundle\Entity\Image
*/
public function getMainImage()
{
return $this->mainImage;
}
}
Of course the big one, my Classified Form !
<?php
namespace Gac\AdsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Gac\MediasBundle\Form\ImageType;
use Gac\VehiclesBundle\Entity\VehicleBrand;
use Gac\VehiclesBundle\Entity\VehicleModel;
class VehicleAdType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$globalDatas = $options['globalDatas'];
$builder->add('vehicleBrand', EntityType::class, array(
'class' => 'GacVehiclesBundle:VehicleBrand',
'choice_label' => 'brandName',
'placeholder' => 'Please select a brand :',
))
->add('vehicleModel', EntityType::class, array(
'class' => 'GacVehiclesBundle:VehicleModel',
'choice_label' => 'modelName',
))
->add('description', TextareaType::class, array('required' => false));
$builder->add('mainImage', ImageType::class);
$formModifier = function (FormInterface $form, VehicleBrand $vehicleBrand = null) {
$models = null === $vehicleBrand ? array() : $vehicleBrand->getVehicleModels();
$form->add('vehicleModel', EntityType::class, array(
'class' => 'GacVehiclesBundle:VehicleModel',
'choice_label' => 'modelName',
'choices' => $models,
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be your entity, i.e. Brand
$data = $event->getData();
$formModifier($event->getForm(), $data->getVehicleBrand());
}
);
$builder->get('vehicleBrand')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$vehicleBrand = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $vehicleBrand);
}
);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Gac\AdsBundle\Entity\VehicleAd',
'globalDatas' => null,
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'gac_adsbundle_vehiclead';
}
}
The classified creation controller
<?php
namespace Gac\AdsBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Gac\MediasBundle\Entity\Image;
use Gac\MediasBundle\Form\ImageType;
use Gac\AdsBundle\Entity\VehicleAd;
use Gac\AdsBundle\Form\VehicleAdType;
use Gac\VehiclesBundle\Entity\Vehicle;
use Gac\VehiclesBundle\Form\VehicleType;
use Gac\VehiclesBundle\Entity\VehicleModel;
use Gac\VehiclesBundle\Entity\VehicleBrand;
class ManageController extends Controller
{
public function sellvehicleAction(Request $request)
{
$this->denyAccessUnlessGranted('ROLE_USER');
$thisuser = $this->get('security.token_storage')->getToken()->getUser();
$em = $this->getDoctrine()->getManager();
// Create new ad and load a "mainImage"
$classified = new VehicleAd($thisuser);
$oneImage = new Image($thisuser);
$classified->setMainImage($oneImage);
$globalDatas = $this->container->get('gac_general.globaldatas'); // Always pass globalDatas VAR to any created form in the application
$form = $this->createForm(VehicleAdType::class, $classified, array(
'globalDatas' => $globalDatas,
));
$form->handleRequest($request);
if ($request->isMethod('POST') && $form->isValid()) {
$em->persist($classified);
$em->flush();
$id = $classified->getId();
return $this->redirectToRoute('gac_ads_previewvehiclead', array('id' => $id));
}
return $this->render('ads/manage/sellvehicle.html.twig', array(
'form' => $form->createView()
));
}
}
And finally the view of my classified form
{% extends 'ads/layout.html.twig' %}
{% block bundle_content %}
<form action="{{ path('gac_ads_sellvehicle') }}" method="post">
<div class="row justify-content-center">
<div class="col-md-8">
{{ form_errors(form) }}
<div class="card mb-3">
<div class="card-body">
<p class="text-primary"><strong>{{ 'mandatory informations'|trans|capitalize }} :</strong></p>
{{ form_row(form.vehicleBrand, {'label': 'Vehicle Brand :'}) }}
{{ form_row(form.vehicleModel, {'label': 'Vehicle Model :'}) }}
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<p class="text-primary">{{ 'image'|trans|capitalize }} :</p>
<div id="toto">
{{ form_row(form.mainImage.imageFile) }}
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<p class="text-primary">{{ 'other parameters'|trans|capitalize }} ({{ 'optional'|trans }}) :</p>
<p class="text-primary">{{ 'tell us a few words about your vehicle'|trans|capitalize }} ({{ 'optional'|trans }}) :</p>
{{ form_row(form.description, {'label': 'Description :'}) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-body">
{{ form_rest(form) }}
</div>
</div>
<button type="submit" class="btn btn-block btn-primary">{{ 'preview your ad'|capitalize }}</button>
</div>
</div>
</form>
{% endblock bundle_content %}
I must say that before asking, I did a lot of searching by myself (tests ...) and tons of google search. I know that there is some tricky stuff with Vich Uploader but I managed everything because ... it works for one file.
So the question is : can you help me make my upload and my form work when embedded in another one ?
Thanks a lot !
Regards
Try to do this in "classified" form: ...add('description', textAreaType::class) ->add('mainImage', ImageType::class); Maybe,using twice FormBuilder instance "$builder" causes problemes. In any case: see here Formulaires Imbriqués. I did so and it works great with embedded forms using vich
I am running Symfony 2.3 with Doctrine and these three (relevant) entities: Publication, Author and AuthorPublication.
Both, Author and Publication have a Many-to-One relationship to AuthorPublication (so it is basically a Many-to-Many relation between Author and Publication but I need the AuthorPublication Entity to order the authors of a publication)
I want to have a form where a user can create a new publication and choose as many authors for that publication as he wants.
I studied this article: How to Embed a Collection of Forms but I do not understand how to apply that to my problem because of the AuthorPublication entity which lies in between.
Relevant Code:
Publication
<?php
namespace ind\PubBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="ind\PubBundle\Repository\PublicationRepository")
* #ORM\Table("publications")
*/
class Publication {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $pid;
/**
* #ORM\OneToMany(targetEntity="AuthorPublication", mappedBy="publication")
* #ORM\OrderBy({"order_id" = "ASC"})
*/
protected $publicationAuthors;
//some more attributes + getters/seters
?>
Author
<?php
namespace ind\PubBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table("aid_author")
*/
class Author {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $aid;
/**
* #ORM\Column(type="string", length=255)
*/
protected $author_surname;
/**
* #ORM\Column(type="string", length=255)
*/
protected $author_forename;
/**
* #ORM\OneToMany(targetEntity="AuthorPublication", mappedBy="author")
*/
protected $authorPublication;
?>
AuthorPublication
<?php
namespace ind\PubBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table("aid_pid")
*/
class AuthorPublication {
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $aid;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $pid;
/**
* #ORM\Column(type="integer")
*/
protected $order_id;
/**
* #ORM\ManyToOne(targetEntity="Publication", inversedBy="publicationAuthors")
* #ORM\JoinColumn(name="pid", referencedColumnName="pid")
*/
protected $publication;
/**
* #ORM\ManyToOne(targetEntity="Author", inversedBy="authorPublication")
* #ORM\JoinColumn(name="aid", referencedColumnName="aid")
*/
protected $author;
?>
You have to make an AuthorPublicationType form. You put field author as an 'entity' and your others fields...
You make your PublicationType including AuthorPublication (Embedded Forms).
Then you can add new AuthorPublication with prototype and very simple javascript.
Note that when you have to save your entity Publication with an authorPublication attribut null first. And after update Publication with authorPublication defined by using temporary ArrayCollection in Publication and LifecycleCallbacks for example.
EDIT : Example.
My entities are Sport OneToMany SportParam ManyToOne ConfigParam. SportParam is composed of a Sport, a ConfigParam and a value.
I want to create a new Sport with multiple ConfigParam :
SportParamType :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ConfigParam', 'entity', array(
'class' => 'PPHBSportScoringBundle:ConfigParam',
'property' => 'nom',
'multiple' => false,
'label'=>'Paramètre'
))
->add('valeur','number',array('precision'=>2))
;
}
SportType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nom')
->add('SportParams', 'collection', array(
'type'=> new SportParamType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference'=> false
))
;
}
My form.html.twig :
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.nom) }}
<ul class=SportParams data-prototype="{{ form_widget(form.SportParams.vars.prototype)|e }}">
{% for param in form.SportParams %}
<li>
{{ form_errors(param.ConfigParam) }}
{{ form_widget(param.ConfigParam) }}
{{ form_errors(param.valeur) }}
{{ form_widget(param.valeur) }}
</li>
{% endfor %}
</ul>
<input type="submit" class="btn btn-primary" />
{{ form_end(form) }}
My Javascript refine because it contains more code (AJAX call). It maybe contains some mistake. If it's not clear have a look to the documentation :
<script type="text/javascript">
var $container = $('ul.SportParams');
// button to add a new SportParam
var $addSportParamLink = $('Ajouter un paramètre');
var $newLinkLi = $('<li></li>').append($addSportParamLink);
$(document).ready(function() {
//delete button on each existing SportParam
$container.find('li').each(function() {
addParamFormDeleteLink($(this));
});
//add button
$container.append($newLinkLi);
// adding a new form when cliking Add button
$addSportParamLink.on('click',function(e) {
e.preventDefault(); // évite qu'un #apparaisse dans l'URL
var index = $container.children().length-1;
addParamForm($container,$newLinkLi);
var bAffiche;
return false;
});
// adding a new form SportParam
function addParamForm($container, $newLinkLi) {
var $prototype = $container.attr('data-prototype');
var newForm = $prototype.replace(/__name__/g, $container.children().length-1);
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
addParamFormDeleteLink($newFormLi);
}
function addParamFormDeleteLink($paramFormLi){
var $removeFormA = $('Supprimer');
$paramFormLi.append($removeFormA);
$removeFormA.on('click', function(e) {
e.preventDefault();
$paramFormLi.remove();
});
}
});
</script>
Sport Entity call's back :
/**
* sport
*
* #ORM\Table()
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ORM\Entity(repositoryClass="SportRepository")
*/
class Sport
{
...Entity attribut...
/**
* #var ArrayCollection
*
* To save Sport without SportParams
*/
private $SportParamsTMP;
...getter and setter ...
/**
* #ORM\PrePersist
*/
public function saveSportParams()
{
$this->SportParamsTMP = $this->SportParams;
$this->SportParams = null;
}
/**
* #ORM\PostPersist
*/
public function restoreSportParams()
{
if ($this->SportParamsTMP == !null) {
$this->SportParams = $this->SportParamsTMP;
}
$this->SportParamsTMP = null;
}
}
Finally the controller 's function to add a new Sport :
public function addAction()
{
$sport = new Sport();
$form = $this->createForm(new SportType(), $sport);
$request = $this->getRequest();
if ($request->getMethod() == "POST") {
$form->bind($request);
if($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($sport);
//saving sport without parameter
$em->flush();
//updating sport with parameter
$em->flush();
return $this->redirect($this->generateUrl('pphb_sport_liste'));
}
}
return $this->render('PPHBSportScoringBundle:Championnat/Sport:add.html.twig', array(
'form' => $form->createView(),
));
}
I hope it help.
Don't know if it's the best way to do it, but it's working for me. If something to improve let me know please.
I have user model + user type, register model + register type... When i Execute it id doesn't validate user (inner data) model. Code below...
User model:
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Security\Core\User\UserInterface;
use Serializable;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="Site\MainBundle\Entity\Repository\UserRepository")
*/
class User implements UserInterface, Serializable
{
/**
* #return array|\Symfony\Component\Security\Core\User\Role[]
*/
public function getRoles()
{
return array('ROLE_USER');
}
/**
*
*/
public function eraseCredentials()
{
}
/**
* #return string
*/
public function serialize()
{
return serialize(array($this->id));
}
/**
* #param string $serialized
*/
public function unserialize($serialized)
{
list ($this->id,) = unserialize($serialized);
}
/**
*
*/
public function __construct()
{
$this->isActive = false;
$this->salt = md5(uniqid(null, true));
}
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function getID()
{
return $this->id;
}
/**
* #var string
*
* #ORM\Column(name="username", type="string", length=255, nullable=false)
* #Assert\NotBlank(message="User name cannot be blank.")
*/
private $username;
public function setUserName($userName)
{
$this->username = $userName;
return $this;
}
public function getUserName()
{
return $this->username;
}
/**
* #var string
*
* #ORM\Column(name="password", type="string", length=255, nullable=false)
* #Assert\NotBlank(message="Password cannot be blank.")
*/
private $password;
public function setPassword($password)
{
$this->password = $password;
return $this;
}
public function getPassword()
{
return $this->password;
}
/**
* #var string
*
* #ORM\Column(name="salt", type="string", length=255, nullable=false)
*/
private $salt;
public function setSalt($salt)
{
$this->salt = $salt;
return $this;
}
public function getSalt()
{
return $this->salt;
}
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=255, nullable=false)
* #Assert\NotBlank(message="E-Mail cannot be blank.")
* #Assert\Email(message="Invalid email address.")
*/
private $email;
public function setEmail($email)
{
$this->email = $email;
return $this;
}
public function getEmail()
{
return $this->email;
}
/**
* #var bool
*
* #ORM\Column(name="isActive", type="boolean", nullable=false)
*/
private $isActive;
public function setIsActive($isActive)
{
$this->isActive = $isActive;
return $this;
}
public function getIsActive()
{
return $this->isActive;
}
}
User Type:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', 'text', array('label' => 'User name'))
->add(
'password',
'repeated',
array(
'label' => 'Password',
'first_name' => 'password',
'second_name' => 'confirm',
'type' => 'password'
)
)
->add('email', 'text', array('label' => 'E-Mail'));
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Site\MainBundle\Entity\User',
'required' => false
)
);
}
/**
* #return string
*/
public function getName()
{
return 'main_user';
}
}
Register model:
use Symfony\Component\Validator\Constraints as Assert;
use Site\MainBundle\Entity\User;
/**
* Register
*/
class Register
{
/**
* #var Site\MainBundle\Entity\User
*
* #Assert\Type(type="Site\MainBundle\Entity\User")
* #Assert\Valid()
*/
protected $user;
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
public function getUser()
{
return $this->user;
}
/**
* #var boolean
*
* #Assert\NotBlank(message="No terms accepted.")
* #Assert\True(message="You have to accept terms to be registered.")
*/
protected $terms;
public function setTerms($terms)
{
$this->terms = (Boolean)$terms;
return $this;
}
public function getTerms()
{
return $this->terms;
}
}
Register Type:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Site\MainBundle\Form\UserType;
class RegisterType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', new UserType())
->add('terms', 'checkbox');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Site\MainBundle\Entity\Register',
'required' => false
)
);
}
/**
* #return string
*/
public function getName()
{
return 'main_register';
}
}
And controller with twig. Part of twig:
{% block page_content %}
<form method="post" action="{{ path('main_register') }}">
{{ form_errors(formRegister.user.username) }}
{{ form_row(formRegister.user.username) }}
{{ form_errors(formRegister.user.password.password) }}
{{ form_row(formRegister.user.password.password) }}
{{ form_errors(formRegister.user.password.confirm) }}
{{ form_row(formRegister.user.password.confirm) }}
{{ form_errors(formRegister.user.email) }}
{{ form_row(formRegister.user.email) }}
{{ form_errors(formRegister.terms) }}
{{ form_row(formRegister.terms) }}
<button type="submit">Register</button>
</form>
{% endblock %}
Register Controller:
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Site\MainBundle\Form\RegisterType;
use Site\MainBundle\Entity\Register;
/**
* RegisterController
*/
class RegisterController extends Controller
{
public function defaultAction()
{
$formRegister = $this->createForm(new RegisterType(), new Register());
if ($this->getRequest()->isMethod('post')) {
$formRegister->handleRequest($this->getRequest());
if ($formRegister->isValid()) {
echo 'form is valid';
}
}
$twig = 'SiteMainBundle:register:default.html.twig';
$data = array(
'formRegister' => $formRegister->createView()
);
return $this->render($twig, $data);
}
}
Where can be problem? Help please with it.
Updated
echo "<pre>";
echo $formRegister->getErrorsAsString();
echo '</pre>';
says:
user:
username:
No errors
password:
password:
No errors
confirm:
No errors
email:
No errors
terms:
ERROR: No terms accepted.
ERROR: You have to accept terms to be registered.
As you can see model User is not validated AT ALL... =\
Found, finally.
Bug is in this part. I wanted to disable this way browser validation...
'required' => false
BUT. This "guys" (I mean developers),compare two things in one setting. So if we set 'required' to false our fields won't be validated. And if true it will be validate BUT ALSO WITH browser validator.. =\ Maybe someone know how to avoid it (browser validation) NOT by editing twig file (some options or other setting)?
PS: To avoid browser i changed twig.
<form method="post" action="{{ path('main_register') }}" novalidate>
If you wanna do it by code, in your Controller, your code should be like:
class RegisterController extends Controller
{
public function defaultAction()
{
$formRegister = $this->createForm(new RegisterType(), new Register(), array(
'attr'=> array('novalidate'=>'novalidate'),
));
With the therd parámeter of "createForm", you can modified the form (for example: acction, class, etc).
I normaly use:
$form = $this->createForm(new RegistroType(), $usuario, array(
'action' => $this->generateUrl('jgo_registro'),
'method' => 'POST',
'attr'=> array('novalidate'=>'novalidate'),
));