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.
Related
Here is my entity:
<?php
namespace App\Entity\Contact;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="contact_contact")
*/
class Contact
{
/**
* #ORM\Id
* #ORM\Column(type="integer", options={"unsigned":true})
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Assert\NotBlank
* #ORM\Column(type="string", length=40, nullable=true)
*/
private $fname;
/**
* #ORM\Column(type="string", length=40, nullable=true)
*/
private $lname;
public function getId(): ?int
{
return $this->id;
}
public function getFname(): ?string
{
return $this->fname;
}
public function setFname(string $fname): self
{
$this->fname = $fname;
return $this;
}
public function getLname(): ?string
{
return $this->lname;
}
public function setLname(?string $lname): self
{
$this->lname = $lname;
return $this;
}
}
Here is the edit controller action code:
/**
* #Route("/{id}/edit", name="contact_contact_edit", methods={"GET","POST"})
*/
public function edit(Request $request, Contact $contact): Response
{
$form = $this->createForm(ContactType::class, $contact);
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$this->getDoctrine()->getManager()->flush();
}
}
return $this->render('contact/contact/edit.html.twig', [
'contact' => $contact,
'form' => $form->createView(),
]);
}
When I post the form but leave the fname (first name) field empty...I get this error (Symfony\Component\PropertyAccess\Exception\InvalidArgumentException)
Expected argument of type "string", "null" given at property path
"fname".
When creating the entity, the #Assert works as expected and the message says so...but if I leave it blank and update post...bzzzt error.
What am I missing?
EDIT | Here is the form class incase thats doing something?
<?php
namespace App\Form\Contact;
use App\Entity\Contact\Contact;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fname', TextType::class, ['label' => 'First Name'])
->add('lname', TextType::class, ['label' => 'Last Name']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Contact::class,
'min_entry' => false,
// NOTE: Must be added to every form class to disable HTML5 validation
'attr' => ['novalidate' => 'novalidate']
]);
$resolver->setAllowedTypes('min_entry', 'bool');
}
}
That's one of the reasons you should avoid allowing the form component changing your entities directly. It will set the data, and then validate it. So it's totally possible for the entity to be in an invalid state after the form has been submitted.
Anyway, you can specify what an empty value should be:
->add('fname', TextType::class, ['label' => 'First Name', 'empty_data' => ''])
I try to create a Symfony Custom type extending the core "entity" type.
But I want to use it with Select2 version 4.0.0 (ajax now works with "select" html element and not with hidden "input" like before).
This type should create an empty select instead of the full list of entities by the extended "entity" type.
This works by setting the option (see configureOption):
'choices'=>array()
By editing the object attached to the form it should populate the select with the current data of the object. I solved this problem but just for the view with the following buildView method ...
Select2 recognize the content of the html "select", and does its work with ajax.
But when the form is posted back, Symfony doesn't recognize the selected choices, (because there were not allowed ?)
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "user": The choice "28" does not exist or is not unique
I tried several methods using EventListeners or Subscribers but I can't find a working configuration.
With Select2 3.5.* I solved the problem with form events and overriding the hidden formtype, but here extending the entitytype is much more difficult.
How can I build my type to let it manage the reverse transformation of my entites ?
Custom type :
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
class AjaxEntityType extends AbstractType
{
protected $router;
public function __construct($router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setAttribute('attr',array_merge($options['attr'],array('class'=>'select2','data-ajax--url'=>$this->router->generate($options['route']))));
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = $form->getConfig()->getAttribute('attr');
$choices = array();
$data=$form->getData();
if($data instanceOf \Doctrine\ORM\PersistentCollection){$data = $data->toArray();}
$values='';
if($data != null){
if(is_array($data)){
foreach($data as $entity){
$choices[] = new ChoiceView($entity->getAjaxName(),$entity->getId(),$entity,array('selected'=>true));
}
}
else{
$choices[] = new ChoiceView($data->getAjaxName(),$data->getId(),$data,array('selected'=>true));
}
}
$view->vars['choices']=$choices;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(array('route'));
$resolver->setDefaults(array('choices'=>array(),'choices_as_value'=>true));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'ajax_entity';
}
}
Parent form
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AlarmsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name','text',array('required'=>false))
->add('user','ajax_entity',array("class"=>"AppBundle:Users","route"=>"ajax_users"))
->add('submit','submit');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Alarms','validation_groups'=>array('Default','form_user')));
}
/**
* #return string
*/
public function getName()
{
return 'alarms';
}
}
Problem solved.
The solution is to recreate the form field with 'choices'=>$selectedChoices in both PRE_SET_DATA and PRE_SUBMIT FormEvents.
Selected choices can be retrived from the event with $event->getData()
Have a look on the bundle I created, it implements this method :
Alsatian/FormBundle - ExtensibleSubscriber
Here is my working code which adds to users (EntityType) related to tag (TagType) ability to fill with options from AJAX calls (jQuery Select2).
class TagType extends AbstractType
{
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$modifyForm = function ($form, $users) {
$form->add('users', EntityType::class, [
'class' => User::class,
'multiple' => true,
'expanded' => false,
'choices' => $users,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($modifyForm) {
$modifyForm($event->getForm(), $event->getData()->getUsers());
}
);
$userRepo = $this->userRepo; // constructor injection
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($modifyForm, $userRepo) {
$userIds = $event->getData()['users'] ?? null;
$users = $userIds ? $userRepo->createQueryBuilder('user')
->where('user.id IN (:userIds)')->setParameter('userIds', $userIds)
->getQuery()->getResult() : [];
$modifyForm($event->getForm(), $users);
}
);
}
//...
}
here's my approach based on Your bundle just for entity type in one formtype.
Usage is
MyType extends ExtensibleEntityType
(dont forget parent calls on build form and configure options)
and the class itself
abstract class ExtensibleEntityType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private EntityManagerInterface $entityManager;
/**
* ExtensibleEntityType constructor.
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function getParent()
{
return EntityType::class;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'preSetData']);
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit'], 50);
}
/**
* #param FormEvent $event
*/
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$parent = $event->getForm()->getParent();
$options = $form->getConfig()->getOptions();
if (!$options['pre_set_called']) {
$options['pre_set_called'] = true;
$options['choices'] = $this->getChoices($options, $event->getData());
$parent->add($form->getName(), get_class($this), $options);
}
}
/**
* #param FormEvent $event
*/
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$parent = $event->getForm()->getParent();
$options = $form->getConfig()->getOptions();
if (!$options['pre_submit_called']) {
$options['pre_submit_called'] = true;
$options['choices'] = $this->getChoices($options, $event->getData());
$parent->add($form->getName(), get_class($this), $options);
$newForm = $parent->get($form->getName());
$newForm->submit($event->getData());
}
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'multiple' => true,
'expanded' => true,
'choices' => [],
'required' => false,
'pre_set_called' => false,
'pre_submit_called' => false,
'validation_groups' => false,
]);
}
/**
* #param array $options
* #param $data
* #return mixed
*/
public function getChoices(array $options, $data)
{
if ($data instanceof PersistentCollection) {
return $data->toArray();
}
return $this->entityManager->getRepository($options['class'])->findBy(['id' => $data]);
}
}
I have a Symfony 2 application, with a form that needs to store a reference to another entity (project) in a hidden field. The project entity is passed in via the form options, my plan was to have a field of the type 'hidden' that simply contains the entity id, this should then be transformed into a project entity on when the form is submitted.
I'm going about this by using a model transformer to transform between the entity to a string (it's ID). However when I try to view the form, I get the following error:
The form's view data is expected to be an instance of class Foo\BarBundle\Entity\Project, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Foo\BarBundle\Entity\Project.
Here is my form class:
<?php
namespace Foo\BarBundle\Form\SED\Waste;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Foo\BarBundle\Form\DataTransformer\EntityToEntityIdTransformer;
/**
* Class WasteContractorEntryType
* #package Foo\BarBundle\Form\CommunityInvestment\Base
*/
class WasteContractorEntryType extends AbstractType
{
protected $name;
protected $type;
protected $phase;
protected $wasteComponent;
public function __construct($formName, $type, $phase, $wasteComponent)
{
$this->name = $formName;
$this->type = $type;
$this->phase = $phase;
$this->wasteComponent = $wasteComponent;
}
/**
* #return mixed
*/
public function getType()
{
return $this->type;
}
/**
* #return mixed
*/
public function getPhase()
{
return $this->phase;
}
/**
* #return mixed
*/
public function getProject()
{
return $this->project;
}
/**
* #return mixed
*/
public function getWasteComponent()
{
return $this->wasteComponent;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$em = $options['em'];
$wasteComponentTransformer = new EntityToEntityIdTransformer($em,
'Foo\BarBundle\Entity\SED\Waste\WasteComponent');
$projectTransformer = new EntityToEntityIdTransformer($em, 'Foo\BarBundle\Entity\Project');
$builder->add('id', 'hidden');
$builder->add(
$builder->create('project', 'hidden', array(
'data' => $options['project'],
'by_reference' => false
))
->addModelTransformer($projectTransformer)
);
$builder->add(
$builder->create('wasteComponent', 'hidden', array(
'data' => $this->getWasteComponent()
))
->addModelTransformer($wasteComponentTransformer)
);
$builder->add('phase', 'hidden', array(
'data' => $this->getPhase()
));
$builder->add('type', 'hidden', array(
'data' => $this->getType()
));
$builder->add('percentDivertedFromLandfill', 'text', array());
$builder->add('wasteContractor', 'entity', array(
'class' => 'Foo\BazBundle\Entity\Contractor',
'property' => 'name',
'attr' => array(
'class' => 'js-select2'
)
));
}
public function getName()
{
return $this->name;
}
/**
* {#inheritDoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => true,
'data_class' => 'Foo\BarBundle\Entity\SED\Waste\WasteContractorEntry'
))
->setRequired(array(
'em',
'project'
))
->setAllowedTypes(array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'project' => 'Foo\BarBundle\Entity\Project'
));
}
}
And my model transformer class:
<?php
namespace Foo\BarBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
class EntityToEntityIdTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #var Entity class
*/
protected $entityClass;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om, $className)
{
$this->om = $om;
$this->entityClass = $className;
}
protected function getEntityClass()
{
return $this->entityClass;
}
/**
* Transforms an object (project) to a string (id).
*
* #param Project|null $issue
* #return string
*/
public function transform($entity)
{
if (null === $entity) {
return "";
}
return $entity->getId();
}
/**
* Transforms a string (id) to an object (project).
*
* #param string $id
*
* #return Issue|null
*
* #throws TransformationFailedException if object (project) is not found.
*/
public function reverseTransform($id)
{
if (!$id) {
return null;
}
$entity = $this->om
->getRepository($this->getEntityClass())
->find($id);
if (null === $entity) {
throw new TransformationFailedException(sprintf(
'An entity of class %s with id "%s" does not exist!',
$this->getEntityClass(),
$id
));
}
return $entity;
}
}
I've tried using a adding the transformer as a view transformer instead of a model transformer, however then I just get a slightly different error:
The form's view data is expected to be an instance of class
Foo\BarBundle\Entity\Project, but is a(n) integer. You can avoid this
error by setting the "data_class" option to null or by adding a view
transformer that transforms a(n) integer to an instance of
Foo\BarBundle\Entity\Project.
It seems that setting 'data_class' to null as suggested by the exception message above is the solution. I had previously rejected this as it seems counter-intuitive when we know that the purpose of the field is to reference a project entity.
With the 'data_class' option set to null, the hidden project field contains the project id, and upon submission, calling getProject() on the created entity returns the correct project object.
I am currently working on a symfony2.3 project with doctrine2 trying to implement personal translations management in the Sonata backend.
Translations are based on the doctrine2 translatable behaviour model: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/translatable.md
and more precisely Personal Translations.
The admin form is using the symfony2.3 version of TranslatedFieldType.php.
My entity class is as follows:
<?php
namespace Hr\OnlineBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #Gedmo\TranslationEntity(class="Hr\OnlineBundle\Entity\CategoryTranslation")
*/
class Category
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #Gedmo\Translatable
* #ORM\Column(length=64)
*/
private $title;
/**
* #Gedmo\Translatable
* #ORM\Column(type="text", nullable=true)
*/
private $description;
/**
* #ORM\OneToMany(
* targetEntity="CategoryTranslation",
* mappedBy="object",
* cascade={"persist", "remove"}
* )
*/
private $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
}
public function getTranslations()
{
return $this->translations;
}
public function addTranslation(CategoryTranslation $t)
{
if (!$this->translations->contains($t)) {
$this->translations[] = $t;
$t->setObject($this);
}
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getDescription()
{
return $this->description;
}
public function __toString()
{
return $this->getTitle();
}
}
The related Personal Translation class is this:
<?php
namespace Hr\OnlineBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation;
/**
* #ORM\Entity
* #ORM\Table(name="category_translations",
* uniqueConstraints={#ORM\UniqueConstraint(name="lookup_unique_idx", columns={
* "locale", "object_id", "field"
* })}
* )
*/
class CategoryTranslation extends AbstractPersonalTranslation
{
/**
* Convinient constructor
*
* #param string $locale
* #param string $field
* #param string $value
*/
public function __construct($locale, $field, $value)
{
$this->setLocale($locale);
$this->setField($field);
$this->setContent($value);
}
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="translations")
* #ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $object;
}
The admin form class is this:
<?php
namespace Hr\OnlineBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Hr\OnlineBundle\Form\Type\TranslatedFieldType;
class CategoryAdmin extends Admin
{
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('title', 'translatable_field', array(
'field' => 'title',
'personal_translation' => 'Hr\OnlineBundle\Entity\CategoryTranslation',
'property_path' => 'translations',
))
->end();
}
// Fields to be shown on filter forms
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
;
}
// Fields to be shown on lists
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
;
}
}
The admin form is using the following form type class:
<?php
namespace Hr\OnlineBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber;
class TranslatedFieldType extends AbstractType
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if(! class_exists($options['personal_translation']))
{
Throw new \InvalidArgumentException(sprintf("Unable to find personal translation class: '%s'", $options['personal_translation']));
}
if(! $options['field'])
{
Throw new \InvalidArgumentException("You should provide a field to translate");
}
$subscriber = new addTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options);
$builder->addEventSubscriber($subscriber);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'remove_empty' => true,
'csrf_protection'=> false,
'field' => false,
'personal_translation' => false,
'locales'=>array('en', 'fr', 'de'),
'required_locale'=>array('en'),
'widget'=>'text',
'entity_manager_removal'=>true,
));
}
public function getName()
{
return 'translatable_field';
}
}
and the corresponding Event Listener:
<?php
namespace Hr\OnlineBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\FormError;
class AddTranslatedFieldSubscriber implements EventSubscriberInterface
{
private $factory;
private $options;
private $container;
public function __construct(FormFactoryInterface $factory, ContainerInterface $container, Array $options)
{
$this->factory = $factory;
$this->options = $options;
$this->container = $container;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that we want to listen on the form.pre_set_data
// , form.post_data and form.bind_norm_data event
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::POST_BIND => 'postBind',
FormEvents::BIND => 'bindNormData'
);
}
private function bindTranslations($data)
{
//Small helper function to extract all Personal Translation
//from the Entity for the field we are interested in
//and combines it with the fields
$collection = array();
$availableTranslations = array();
foreach($data as $Translation)
{
if(strtolower($Translation->getField()) == strtolower($this->options['field']))
{
$availableTranslations[ strtolower($Translation->getLocale()) ] = $Translation;
}
}
foreach($this->getFieldNames() as $locale => $fieldName)
{
if(isset($availableTranslations[ strtolower($locale) ]))
{
$Translation = $availableTranslations[ strtolower($locale) ];
}
else
{
$Translation = $this->createPersonalTranslation($locale, $this->options['field'], NULL);
}
$collection[] = array(
'locale' => $locale,
'fieldName' => $fieldName,
'translation' => $Translation,
);
}
return $collection;
}
private function getFieldNames()
{
//helper function to generate all field names in format:
// '<locale>' => '<field>|<locale>'
$collection = array();
foreach($this->options['locales'] as $locale)
{
$collection[ $locale ] = $this->options['field'] .":". $locale;
}
return $collection;
}
private function createPersonalTranslation($locale, $field, $content)
{
//creates a new Personal Translation
$className = $this->options['personal_translation'];
return new $className($locale, $field, $content);
}
public function bindNormData(FormEvent $event)
{
//Validates the submitted form
$data = $event->getData();
$form = $event->getForm();
$validator = $this->container->get('validator');
foreach($this->getFieldNames() as $locale => $fieldName)
{
$content = $form->get($fieldName)->getData();
if(
NULL === $content &&
in_array($locale, $this->options['required_locale']))
{
$form->addError(new FormError(sprintf("Field '%s' for locale '%s' cannot be blank", $this->options['field'], $locale)));
}
else
{
$Translation = $this->createPersonalTranslation($locale, $fieldName, $content);
$errors = $validator->validate($Translation, array(sprintf("%s:%s", $this->options['field'], $locale)));
if(count($errors) > 0)
{
foreach($errors as $error)
{
$form->addError(new FormError($error->getMessage()));
}
}
}
}
}
public function postBind(FormEvent $event)
{
//if the form passed the validattion then set the corresponding Personal Translations
$form = $event->getForm();
$data = $form->getData();
$entity = $form->getParent()->getData();
foreach($this->bindTranslations($data) as $binded)
{
$content = $form->get($binded['fieldName'])->getData();
$Translation = $binded['translation'];
// set the submitted content
$Translation->setContent($content);
//test if its new
if($Translation->getId())
{
//Delete the Personal Translation if its empty
if(
NULL === $content &&
$this->options['remove_empty']
)
{
$data->removeElement($Translation);
if($this->options['entity_manager_removal'])
{
$this->container->get('doctrine.orm.entity_manager')->remove($Translation);
}
}
}
elseif(NULL !== $content)
{
//add it to entity
$entity->addTranslation($Translation);
if(! $data->contains($Translation))
{
$data->add($Translation);
}
}
}
}
public function preSetData(FormEvent $event)
{
//Builds the custom 'form' based on the provided locales
$data = $event->getData();
$form = $event->getForm();
// During form creation setData() is called with null as an argument
// by the FormBuilder constructor. We're only concerned with when
// setData is called with an actual Entity object in it (whether new,
// or fetched with Doctrine). This if statement let's us skip right
// over the null condition.
if (null === $data)
{
return;
}
foreach($this->bindTranslations($data) as $binded)
{
$form->add($this->factory->createNamed(
$binded['fieldName'],
$this->options['widget'],
$binded['translation']->getContent(),
array(
'label' => $binded['locale'],
'required' => in_array($binded['locale'], $this->options['required_locale']),
'auto_initialize' => false,
)
));
}
}
}
So, the form shows the three fields for the three specified locales, which is fine, but when submitting the form the following error occurs:
FatalErrorException: Error: Call to a member function getField() on a non-object in C:\wamp\www\hronline\src\Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber.php line 47
in C:\wamp\www\hronline\src\Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber.php line 47
at ErrorHandler->handleFatal() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Debug\ErrorHandler.php line 0
at AddTranslatedFieldSubscriber->bindTranslations() in C:\wamp\www\hronline\src\Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber.php line 136
at AddTranslatedFieldSubscriber->postBind() in C:\wamp\www\hronline\app\cache\dev\classes.php line 1667
at ??call_user_func() in C:\wamp\www\hronline\app\cache\dev\classes.php line 1667
at EventDispatcher->doDispatch() in C:\wamp\www\hronline\app\cache\dev\classes.php line 1600
at EventDispatcher->dispatch() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\EventDispatcher\ImmutableEventDispatcher.php line 42
at ImmutableEventDispatcher->dispatch() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Form\Form.php line 631
at Form->submit() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Form\Form.php line 552
at Form->submit() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Form\Form.php line 645
at Form->bind() in C:\wamp\www\hronline\vendor\sonata-project\admin-bundle\Sonata\AdminBundle\Controller\CRUDController.php line 498
at CRUDController->createAction() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2844
at ??call_user_func_array() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2844
at HttpKernel->handleRaw() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2818
at HttpKernel->handle() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2947
at ContainerAwareHttpKernel->handle() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2249
at Kernel->handle() in C:\wamp\www\hronline\web\app_dev.php line 28
at ??{main}() in C:\wamp\www\hronline\web\app_dev.php line 0
It appears that on this line of the event listener AddTranslatedFieldSubscriber:
if(strtolower($Translation->getField()) == strtolower($this->options['field']))
the $Translation variable comes as a string (e.g. string 'Lorem' (length=5)) instead of an object.
For some reason the form data is converted to an array of strings instead of an array of objects of type CategoryTranslation.
What could be the reason for this? Thanks!
Do yourself a favour and don't use the Gedmo Translatable behaviour. It is very buggy, slow and has a lot of weird edge cases. I recommend using the KNP translatable behaviour which is quite good (requires PHP 5.4) or the Prezent Translatable which is similar but can work on PHP 5.3. Disclaimer: I wrote that last one. It's beta but works fine. I just added the documentation for it.
You can use the a2lix bundle to integrate it all in Sonata Admin.
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. :)