My entity looks like this
<?php
namespace App\Entity;
use App\Repository\AnimauxRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=AnimauxRepository::class)
*/
class Animaux
{
// ...
/**
* #ORM\Column(type="string")
*/
private $photo;
// ...
public function getPhoto()
{
return $this->photo;
}
public function setPhoto($photo)
{
$this->photo = $photo;
return $this;
}
// ...
}
My AnimauxType.php looks like this
<?php
// AnimauxType.php
namespace App\Form;
use App\Entity\Animaux;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class AnimauxType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('photo', FileType::class, [
'data_class'=>null,
'label' => 'Photo (jpg ou png)',
'mapped' => false,
'required' => false,
'constraints' => [
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'image/jpg',
'image/jpeg',
'image/png',
'image/gif',
'image/jfif',
],
'mimeTypesMessage' => 'Please upload a valid image',
])
],
])
// ...
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver -> setDefault('data_class', Animaux::class);
}
}
My AnimauxController.php looks like this
public function new(Request $request, SluggerInterface $slugger): Response
{
$animaux = new Animaux();
$form = $this->createForm(AnimauxType::class, $animaux);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var UploadedFile $photo */
$photo = $form->get('photo')->getData();
if ($photo) {
$originalFilename = pathinfo($photo->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename.'-'.uniqid().'.'.$photo->guessExtension();
try {
$photo->move(
$this->getParameter('photo_directory'),
$newFilename
);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
$animaux->setPhoto(
new File($this->getParameter('photo_directory').'/'.$animaux->getPhoto())
);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($animaux);
$entityManager->flush();
return $this->redirectToRoute('animaux_index');
}
and finally I've added this in service.yaml
parameters:
photo_directory: '%kernel.project_dir%/public/uploads/photos'
When i try to upload a picture in my form, I get no error but I can't see the picture. Here's how I try to display the picture <img src="{{asset('uploads/photo/' ~ animaux.photo)}}" alt=""> but when i look at the code i get this <img src="/petandconnect/public/uploads/photo/C:\Users\...\Temp\php4EA9.tmp" alt="">
Can't make heads or tails of this.
[VichUploaderBundle] will be your friend in that case. Your browser won't recognize the relative path of a file comming directly from your database. You will need another property photoFile of type File that will be mapped with your property "photo".
Just google the procedure with VichUpploaderBundle and it will work. It is the bundle approved by the community for uploaded file. Also, don't forget to use the cacheManager to remove former photos from the cache or your app will slow down.
Related
Installed easyadminbundle with symfony 4, configured for an entity name Delivery and it has a field associated to another entity name WeeklyMenu:
easy_amin.yaml:
Delivery:
...
form:
fields:
- { property: 'delivered'}
- { property: 'weeklyMenu', type: 'choice', type_options: { choices: null }}
I need a dynamically filtered results of weeklyMenu entity here, so I can get a list of the next days menus and so on. It's set to null now but have to get a filtered result here.
I've read about overriding the AdminController which I stucked with it. I believe that I have to override easyadmin's query builder that listing an associated entity's result.
i've figured out, here is the solution if someone looking for:
namespace App\Controller;
use Doctrine\ORM\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilder;
class AdminController extends EasyAdminController {
public function createDeliveryEntityFormBuilder($entity, $view) {
$formBuilder = parent::createEntityFormBuilder($entity, $view);
$fields = $formBuilder->all();
/**
* #var $fieldId string
* #var $field FormBuilder
*/
foreach ($fields as $fieldId => $field) {
if ($fieldId == 'weeklyMenu') {
$options = [
'attr' => ['size' => 1,],
'required' => true,
'multiple' => false,
'expanded' => false,
'class' => 'App\Entity\WeeklyMenu',
];
$options['query_builder'] = function (EntityRepository $er) {
$qb = $er->createQueryBuilder('e');
return $qb->where($qb->expr()->gt('e.date', ':today'))
->setParameter('today', new \DateTime("today"))
->andWhere($qb->expr()->eq('e.delivery', ':true'))
->setParameter('true', 1)
->orderBy('e.date', 'DESC');
};
$formBuilder->add($fieldId, EntityType::class, $options);
}
}
return $formBuilder;
}
}
so the easyAdmin check if a formbuilder exists with the entity's name i.e. create<ENTITYNAME>FormBuilder(); and you can override here with your own logic.
Another approach to this would be to create new FormTypeConfigurator and overwrite choices and/or labels. And tag it as:
App\Form\Type\Configurator\UserTypeConfigurator:
tags: ['easyadmin.form.type.configurator']
and the configurator looks like this:
<?php
declare(strict_types = 1);
namespace App\Form\Type\Configurator;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\TypeConfiguratorInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormConfigInterface;
final class UserTypeConfigurator implements TypeConfiguratorInterface
{
/**
* {#inheritdoc}
*/
public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig)
{
if ($parentConfig->getData() instanceof User) {
$options['choices'] = User::getUserStatusAvailableChoices();
}
return $options;
}
/**
* {#inheritdoc}
*/
public function supports($type, array $options, array $metadata)
{
return in_array($type, ['choice', ChoiceType::class], true);
}
}
In symfony 2.5.6,
how to change options dynamically in symfony2 form, by example:
// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit');
if (condition) {
//how to change option of 'task' or 'dueDate' by example
//something like this, but addOption doesn't exist and i don't find any usefull method
$builder->get('dueDate')->addOption('read_only', true)
}
}
public function getName()
{
return 'task';
}
}
Need to use event ?
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
Or this
foreach($builder->all() as $key => $field) {
if ($key == 'dueDate')) {
$options = $field->getOptions();
$options = array_merge_recursive($options, array('read_only' => true));
$builder->remove($key);
$builder->add($key, $field->getName(), $options);
}
}
#with 'Could not load type "dueDate"' error when i display my form in a browser!
How to to do? Best practice?
Thanks!
I dont't know what do you mean by 'best practice', but why not to do it like this:
$builder
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit');
$options = [
KEY => VALUE,
....
];
if (condition) {
$options = [
ANOTHER_KEY => ANOTHER_VALUE,
....
];
}
$builder->add('task', TYPE, $options);
Another approach would be to use PRE_SUBMIT event, something like this..
$builder
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit');
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit']);
....
public function preSubmit(FormEvent $event)
{
if (CONDITION) {
$builder->remove('task');
$builder->add('task', TYPE, $NEW_OPTIONS_ARRAY);
}
}
I use this function to update options after a field has been added to a form. It basically means to regenerate the field with the data that we have, add something to the options and re-add the field, re-add its transformers and so
Put this helper function somewhere in a FormHelper class, or wherever you like
/**
* #param FormBuilderInterface $builder
* #param string $fieldName
* #param string $optionName
* #param $optionData
*/
public static function setOptionToExistingFormField(
FormBuilderInterface $builder,
string $fieldName,
string $optionName,
$optionData
): void {
if (!$builder->has($fieldName)) {
// return or throw exception as you wish
return;
}
$field = $builder->get($fieldName);
// Get some things from the old field that we also need on the new field
$modelTransformers = $field->getModelTransformers();
$viewTransformers = $field->getViewTransformers();
$options = $field->getOptions();
$fieldType = get_class($field->getType()->getInnerType());
// Now set the new option value
$options[$optionName] = $optionData;
/**
* Just use "add" again, if it already exists the existing field is overwritten.
* See the documentation of the add() function
* Even the position of the field is preserved
*/
$builder->add($fieldName, $fieldType, $options);
// Reconfigure the transformers (if any), first remove them or we get some double
$newField = $builder->get($fieldName);
$newField->resetModelTransformers();
$newField->resetViewTransformers();
foreach($modelTransformers as $transformer) {
$newField->addModelTransformer($transformer);
}
foreach($viewTransformers as $transformer) {
$newField->addViewTransformer($transformer);
}
}
And then use it like this
$builder
->add('someField', SomeSpecialType::class, [
'label' => false,
])
;
FormHelper::setOptionToExistingFormField($builder, 'someField', 'label', true);
I have used DI in Controller, Repository. All repository extend with BaseRepository. But when using a FormType gives the error.
Error Message:
ContextErrorException: Catchable Fatal Error: Argument 2 passed to Personal\SiteBundle\Repository\BaseRepository::__construct() must be an instance of Memcache, instance of Doctrine\ORM\Mapping\ClassMetadata given, called in /home/personal/www/vendor/doctrine/orm/lib/Doctrine/ORM/Repository/DefaultRepositoryFactory.php on line 75 and defined in /home/personal/www/src/Personal/SiteBundle/Repository/BaseRepository.php line 22
How do I think? Structure as follows
AdminBundle/services.yml
personaladmin.towncontroller:
class: Personal\AdminBundle\Controller\TownController
arguments: ["#personalsite.townrepository", "#doctrine.orm.entity_manager", "#personalsite.townformtype"]
parent: "personaladmin.basecontroller"
SiteBundle/services.yml
services:
memcache:
class: Memcache
calls:
- [addServer , [127.0.0.1, 11211]]
personalsite.baserepository:
class: Personal\SiteBundle\Repository\BaseRepository
arguments:
entityManager: "#doctrine.orm.entity_manager"
memcacheProvider: "#memcache"
personalsite.cityrepository:
class: Personal\SiteBundle\Repository\CityRepository
parent: personalsite.baserepository
personalsite.townrepository:
class: personal\SiteBundle\Repository\TownRepository
parent: personalsite.baserepository
personalsite.townformtype:
class: personal\SiteBundle\Form\TownType
arguments: ["#personalsite.cityrepository"]
BaseRepository.php
<?php
namespace Personal\SiteBundle\Repository;
use Doctrine\ORM\EntityManager;
use \Memcache;
class BaseRepository
{
protected $_memcacheProvider;
/**
* Connection
*
* #var \Doctrine\ORM\EntityManager
*/
protected $_em;
public function __construct(
EntityManager $entityManager,
Memcache $memcachedProvider
)
{
$this->_memcacheProvider = $memcachedProvider;
$this->_em = $entityManager;
}
}
TownController.php
<?php
namespace Personal\AdminBundle\Controller;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Personal\AdminBundle\Controller\BaseController;
use Personal\SiteBundle\Form\TownType;
use Personal\SiteBundle\Entity\Town;
use Personal\SiteBundle\Repository\TownRepository;
/**
* #Route("/town", service="personaladmin.towncontroller")
*/
class TownController extends BaseController
{
protected $_townRepository;
protected $_entityManager;
public function __construct(
TownRepository $townRepository,
EntityManager $entityManager,
TownType $townType
)
{
$this->_townRepository = $townRepository;
$this->_entityManager = $entityManager;
$this->_townType = $townType;
}
/**
* #Route("/list", name="managerv3_town_list")
* #Template()
*/
public function townsAction()
{
$contents = $this->_townRepository->getAll();
return $this->render('PersonalAdminBundle:Town:list.html.twig', array('contents' => $contents));
}
/**
* #Route("/add", name="managerv3_town_add", defaults={"id" = null})
* #Route("/edit/{id}", name="managerv3_town_edit", defaults={"id" = null})
* #Template()
*/
public function townAction(Request $request, $id)
{
if( is_null($id) )
{
$content = new Town();
}
else
{
$content = $this->_townRepository->getSingle(array( 'id' => $id ));
}
$form = $this->createForm($this->_townType, $content);
if($request->getMethod() == 'POST')
{
$form->bind($request);
if($form->isValid())
{
$this->_entityManager->persist($content);
$this->_entityManager->flush();
return $this->redirect( $this->generateUrl('managerv3_town_list') );
}
}
return $this->render('PersonalAdminBundle:Town:form.html.twig', array('form' => $form->createView()));
}
TownRepository.php
<?php
namespace Personal\SiteBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Personal\SiteBundle\Repository\BaseRepository;
use Doctrine\ORM\Query;
/**
* TownRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class TownRepository extends BaseRepository
{
function getAll( $orderBy = array('town.title' => 'ASC'), $hydrateMode = false )
{
$towns =
$this->_em
->createQueryBuilder()
->select('town,city')
->from('PersonalSiteBundle:Town', 'town')
->leftJoin('town.city','city');
foreach( $orderBy as $oKey => $oVal )
{
$towns->orderBy($oKey, $oVal);
}
$result = $towns->getQuery()
->getResult();
return $result;
}
function getSingle( $where = array(), $hydrateMode = null )
{
$content =
$this->_em
->createQueryBuilder()
->select('town,city')
->from('PersonalSiteBundle:Town', 'town')
->leftJoin('town.city', 'city');
foreach( $where as $condKey => $condVal )
{
$parameterKey = 'town_' . $condKey;
$content->andWhere("town.{$condKey} = :{$parameterKey}");
$content->setParameter($parameterKey, $condVal);
}
$result = $content->getQuery();
if( $hydrateMode )
return $result->getSingleResult(Query::HYDRATE_ARRAY);
return $result->getSingleResult();
}
}
TownType.php
<?php
namespace Personal\SiteBundle\Form;
use Personal\SiteBundle\Repository\CityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Personal\SiteBundle\Entity\City;
class TownType extends AbstractType
{
private $_cityrepository;
public function __construct(CityRepository $cityRepository)
{
$this->_cityrepository = $cityRepository;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text', array(
'label' => 'İlçe Adı',
'horizontal_label_class' => 'col-lg-2',
'horizontal_input_wrapper_class' => 'col-lg-6',
'widget_type' => 'inline'
))
->add('slug')
->add('city', 'entity', array(
'empty_value' => 'Şehir Seçiniz',
'class' => 'PersonalSiteBundle:City',
'query_builder' => function(){
return $this->_cityrepository->getAll();
},
'property' => 'title',
'label' => 'Şehir',
'horizontal_label_class' => 'col-lg-2',
'horizontal_input_wrapper_class' => 'col-lg-3',
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Personal\SiteBundle\Entity\Town'
));
}
/**
* #return string
*/
public function getName()
{
return 'personal_sitebundle_town';
}
}
Thanks for your help.
So your BaseRepository needs to extend DoctrineReposotory. Doctrine creates repositories using a factory getRepository method. There is no easy way to coax it to pass memcache along. Instead we need to use setter injection.
use Doctrine\ORM\EntityRepository;
class BaseRepository extends EntityRepository
{
// No constructor needed
// Setter injection
setMemcachedProvider(Memcache $memcachedProvider)
{
$this->_memcacheProvider = $memcachedProvider;
}
In your services.yml file get rid of the BaseRepository services and then edit your repository services:
personalsite.cityrepository:
class: Personal\SiteBundle\Repository\CityRepository
factory_service: 'doctrine.orm.default_entity_manager'
factory_method: 'getRepository'
arguments:
- 'Personal\SiteBundle\Entity\City'
calls:
- [setMemcachedProvider, ['#memcache']]
That should do the trick.
I am a little bit curious as to why you are injecting memcache. I am assuming you have some custom repository code that uses it? Doctrine will ignore it. If you are trying to configure Doctrine to use memcache then that is a different process altogether.
How its possible to join two separated fields (must be separated) in one form (date and time for example) to one entity propery datetime for persisting after form post ?
What is better way ? Data Transofmers ? Form events ? Form Model ? Manual setting all entity properties before persist ?
Entity:
<?php namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="event")
*/
class EventEntity
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
// ...
/**
* #ORM\Column(name="date_time", type="datetime", nullable=false)
*/
protected $datetime;
public function getId()
{
return $this->id;
}
// ...
public function getDateTime()
{
return $this->datetime;
}
public function setDateTime(\DateTime $datetime)
{
$this->datetime = $datetime;
}
}
FormType:
<?php namespace Acme\DemoBundle\Form\Type;
use JMS\DiExtraBundle\Annotation as DI;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('date', 'date', [
'required' => true,
'widget' => 'single_text',
'format' => 'dd.MM.yyyy'
]
)
->add('time', 'time', [
'required' => false,
'widget' => 'single_text'
]
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\EventEntity' //Acme\DemoBundle\Form\Model\EventModel ?
));
}
public function getName()
{
return 'event';
}
}
If you set the date and time widget seperately in the datetime type, then they get seperately rendered, but validated as one field.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('datetime', 'datetime', array(
'date_widget' => 'single_text',
'time_widget' => 'single_text',
'date_format' => 'dd.MM.yyyy',
));
}
I suggest using Pazis solution, since this is the most simple one. But that would also be a perfect job for a DataTransformer:
class MyDataTransformer implements DataTransformerInterface
{
public function transform($value)
{
if (null === $value)
return;
if ($value instanceof \DateTime)
return array(
'date' => $value->format('d.m.Y'),
'time' => $value->format('H:i:s')
);
return null;
}
public function reverseTransform($value)
{
if (null === $value)
return null;
if (is_array($value) && array_key_exists('date', $value) && array_key_exists('time', $value))
return new \DateTime($value['date'] . ' ' . $value['time']);
return null;
}
}
This has the drawback, that you'd need to map every single value in your entity with this transformer, what - for sure - you don't want to. But with small form-tricks, this can be avoided. Therefore you add a subform to your form, which includes a date and a time field and the added Transformer. You'll need to map ("property_path"-option) your DateTime object to this subform or just name it "correctly", so the form framework can map it by name.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('datetime', 'form')
->add('date', 'date', $optionsForDate)
->add('time', 'time', $optionsForTime)
->addViewTransformer(new MyDataTransformer())
);
}
The code may not be perfectly running, but i hope the idea behind splitting one entity property into two (or more) form fields is clear.
héhé, that's a good question.
I would choose the easiest, most generic, reusable solution.
I wouldn't implement methods on my model just for sake of form mapping, but if it makes sense, why not simply using the model api ?
<?php
class EventEntity
{
// assume $this->datetime is initialized and instance of DateTime
public function setDate(\DateTime $date)
{
// i don't know if this works!
$this->datetime->add($this->datetime->diff($date));
}
public function setTime(\DateTime $date)
{
$this->datetime->add($this->datetime->diff($date));
}
}
Names changed due to NDA.
I'm trying to come up with a survey form. Each survey question can have multiple answers/scores, so there's a natural 1:* relationship to them. That said, for the public-facing form, I need to have a 1:1 relationship between the score and the question it relates to, which is what I'm working on now. Right now, the survey is open to the public, so each completed survey is not related to a user.
The interesting parts of my current setup are as follows...
Question:
namespace Acme\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Question
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string question
*
* #ORM\Column(name="question", type="string", length=255)
*/
private $question;
/**
* #var ArrayCollection scores
*
* #ORM\OneToMany(targetEntity="Score", mappedBy="question")
*/
private $scores;
public function __construct()
{
$this->scores = new ArrayCollection();
}
// other getters and setters
/**
* #param $score
*/
public function setScore($score)
{
$this->scores->add($score);
}
/**
* #return mixed
*/
public function getScore()
{
if (get_class($this->scores) === 'ArrayCollection') {
return $this->scores->current();
} else {
return $this->scores;
}
}
}
Those last two are helper methods so I can add/retrieve individual scores. The type checking convolutions were due to an error I encountered here
Score:
namespace Acme\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Score
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var integer $question
*
* #ORM\ManyToOne(targetEntity="Question", inversedBy="scores")
* #ORM\JoinColumn(name="question_id", referencedColumnName="id")
*/
private $question;
/**
* #var float score
*
* #ORM\Column(name="score", type="float")
*/
private $score;
// getters and setters
}
Controller method:
public function takeSurveyAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
$questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
$viewQuestions = array();
foreach ($questions as $question) {
$viewQuestions[] = $question;
$rating = new Score();
$rating->setQuestion($question->getId());
$question->setRatings($rating);
}
$form = $this->createForm(new SurveyType(), array('questions' => $questions));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
foreach ($questions as $q) {
$em->persist($q);
}
$em->flush();
$em->clear();
$url = $this->get('router')->generate('_main');
$response = new RedirectResponse($url);
return $response;
}
}
return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $viewQuestions));
}
My form types....
SurveyType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('questions', 'collection', array('type' => new SurveyListItemType()));
}
public function getName()
{
return 'survey';
}
}
SurveyListItemType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyListItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('rating', new SurveyScoreType());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Question'));
}
public function getName()
{
return 'survey_list_item_type';
}
}
SurveyScoreType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyRatingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('score', 'choice', array('choices' => array(
'0' => '',
'0.5' => '',
'1' => '',
'1.5' => '',
'2' => '',
'2.5' => '',
'3' => '',
'3.5' => '',
'4' => '',
'4.5' => '',
'5' => ''
), 'expanded' => true, 'multiple' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
}
public function getName()
{
return 'survey_score_type';
}
}
Okay, with all of that, I'm getting the following error when Doctrine's EntityManager attempts to flush() in my controller action:
Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /home/kevin/www/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 547 and defined in /home/kevin/www/project/vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php line 47
I believe it has to do with the questions' related scores, as they're supposed to be an array(collection) in Question, but they're individual instances in this case. The only problem is I'm not sure how to fix it.
I'm thinking my form setup may be too complex. All I really need to do is attach each Question.id to each related Score. I'm just not sure the best way to build the form part of it so everything is persisted properly.
I believe your error is here
$rating = new Score();
//...
$question->setRatings($rating);
Usually if you have an ArrayCollection in your Entity, then you have addChildEntity and removeChildEntity methods that add and remove elements from the ArrayCollection.
setRatings() would take an array of entities, rather than a single entity.
Assuming that you do have this method, try
$question->addRating($rating);
I think you have a mistake in your setRating method.
You have
$this->score->add($score);
It should be:
$this->scores->add($score);
I was able to solve it by simply handling the Scores. So, with that approach, I was able to remove SurveyListItemType, and make the following changes:
SurveyType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('scores', 'collection', array('type' => new SurveyRatingType()));
}
public function getName()
{
return 'survey';
}
}
Note how the collection type is now mapped to SurveyRatingType.
SurveyRatingType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyRatingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('score', 'choice', array('choices' => array(
'0' => '',
'0.5' => '',
'1' => '',
'1.5' => '',
'2' => '',
'2.5' => '',
'3' => '',
'3.5' => '',
'4' => '',
'4.5' => '',
'5' => ''
), 'expanded' => true, 'multiple' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
}
public function getName()
{
return 'survey_rating_type';
}
}
And my modified controller action:
public function takeSurveyAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
$questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
$ratings = array();
foreach ($questions as $question) {
$rating = new SurveyRating();
$rating->setQuestion($question);
$ratings[] = $rating;
}
$form = $this->createForm(new SurveyType(), array('ratings' => $ratings));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
foreach ($ratings as $r) {
$em->persist($r);
}
$em->flush();
$em->clear();
$url = $this->get('router')->generate('_main');
$response = new RedirectResponse($url);
return $response;
}
}
return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $questions));
}
I had a feeling I was doing it wrong due to the three form types. That really jumped out as a bad code smell. Thanks to everyone for their patience and attempts at helping. :)