I updated composer on my symfony project, and i could not continue to use my custom "SecurityRolesType".
Any idea to update it ? following, codes and errors.
Error : Notice: Array to string conversion 500 Internal Server Error - ContextErrorException
Here a stack screen capture http://s11.postimg.org/j6nlup4ar/screencapture_stack.jpg
Here a capture of rendered HTML http://s17.postimg.org/l6mmskmof/Screen_Shot_2015_09_28_at_10_41_19_PM.png
<?php
namespace MYP\UserBundle\Form\Type;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\Options;
use Sonata\AdminBundle\Admin\Pool;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class SecurityRolesType extends ChoiceType
{
protected $pool;
/**
* #param Pool $pool
*/
public function __construct(Pool $pool)
{
parent::__construct();
$this->pool = $pool;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$attr = $view->vars['attr'];
if (isset($attr['class']) && empty($attr['class'])) {
$attr['class'] = 'sonata-medium';
}
$view->vars['attr'] = $attr;
$view->vars['read_only_choices'] = $options['read_only_choices'];
//$view->vars['full_name'] = substr($view->vars['full_name'], 0, -2);
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$roles = array();
$rolesReadOnly = array();
$securityContext = $this->pool->getContainer()->get('security.context');
// get roles from the Admin classes
foreach ($this->pool->getAdminServiceIds() as $id) {
try {
$admin = $this->pool->getInstance($id);
} catch (\Exception $e) {
continue;
}
$isMaster = $admin->isGranted('MASTER');
$securityHandler = $admin->getSecurityHandler();
// TODO get the base role from the admin or security handler
$baseRole = $securityHandler->getBaseRole($admin);
foreach ($admin->getSecurityInformation() as $role => $permissions) {
$role = sprintf($baseRole, $role);
if ($isMaster) {
// if the user has the MASTER permission, allow to grant access the admin roles to other users
$roles[$role] = $role;
} elseif ($securityContext->isGranted($role)) {
// although the user has no MASTER permission, allow the currently logged in user to view the role
$rolesReadOnly[$role] = $role;
}
}
}
// get roles from the service container
foreach ($this->pool->getContainer()->getParameter('security.role_hierarchy.roles') as $name => $rolesHierarchy) {
if ($securityContext->isGranted($name)) {
$roles[$name] = $name . ': ' . implode(', ', $rolesHierarchy);
foreach ($rolesHierarchy as $role) {
if (!isset($roles[$role])) {
$roles[$role] = $role;
}
}
}
}
$resolver->setDefaults(array(
'choices' => function (Options $options, $parentChoices) use ($roles) {
return empty($parentChoices) ? $roles : array();
},
'read_only_choices' => function (Options $options) use ($rolesReadOnly) {
return empty($options['choices']) ? $rolesReadOnly : array();
},
'data_class' => null,
//'expanded' => true
));
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return 'choice';
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'myp_security_roles';
}
}
Finally, the fix you can see commented was working after some cache deletion.
$view->vars['full_name'] = substr($view->vars['full_name'], 0, -2);
Related
as soon as I call up the form builder into my unit test I get the following exception:
class EventFormBuilderTest extends TypeTestCase
{
public function testSuccessfullyCreatesForm(): void
{
$formBuilder = new EventFormBuilder($this->factory);
$user = new User();
$actualEvent = new Event();
$form = $formBuilder->buildFormBasedOnUserRole($user, $actualEvent);
I injected a repository into my form to have default options:
class EventType extends AbstractType
{
/** #var LegalServiceRepository */
private $legalServiceRepository;
public function __construct(LegalServiceRepository $legalServiceRepository)
{
$this->legalServiceRepository = $legalServiceRepository;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['services']);
$resolver->setAllowedTypes('services', 'App\Entity\LegalService[]');
$resolver->setDefaults([
'data_class' => Event::class,
'services' => $this->legalServiceRepository->findAllOfTypeEvent(),
'error_mapping' => [
'.' => 'court',
],
]);
}
Then I wrote a custom form builder that allows me to create a different form based on the user's role. The GlobalAdminEventType form extends EventType. I simply needed an extra field.
This works perfectly like expected and i get my options without any error.
class EventFormBuilder
{
/** #var FormFactoryInterface */
private $formFactory;
public function __construct(
FormFactoryInterface $formFactory
) {
$this->formFactory = $formFactory;
}
public function buildFormBasedOnUserRole(User $user, Event $event): FormInterface
{
if (\in_array(RolesEnum::ROLE_SUPER_ADMIN, $user->getRoles(), true)) {
$form = $this->buildForm(GlobalAdminEventType::class, $event);
} else {
$event->setCourt($user->getCourt());
$form = $this->buildForm(EventType::class, $event);
}
return $form;
}
public function buildForm(string $type, Event $event): FormInterface
{
return $this->formFactory->create($type, $event);
}
}
I'm newbie on Symfony. I am trying to update an old project under symfony 2.6 to symfony 3.3.
After multiple bug fixes I am stuck on a point: I have an Error in my EntityRepository.php file with the constructor.
Type error: Too few arguments to function Doctrine\ORM\EntityRepository::__construct(), 1 passed in /Users/.../var/cache/dev/appDevDebugProjectContainer.php on line 3434 and exactly 2 expected
I understand the error, but my EntityRepository file not contain any __construct method. Should I fix something between Symfony 2 and 3 for the consructor to work?
Thanks a lot.
Here is my MilestoneRepository.php file:
namespace MilestonesBundle\Entity\Repository;
use DateTime;
use Doctrine\ORM\EntityRepository;
use Milestones\Entity\Factory\MilestoneFactoryInterface;
use Milestones\Entity\Repository\MilestoneRepositoryInterface;
class MilestoneRepository extends EntityRepository implements MilestoneFactoryInterface, MilestoneRepositoryInterface
{
protected $current = false;
/**
* #see MilestoneFactoryInterface
*/
public function create()
{
$class = $this->getClassName();
return new $class;
}
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
{
if (!$orderBy) {
$orderBy['startsAt'] = 'ASC';
}
return parent::findBy($criteria, $orderBy, $limit, $offset);
}
/**
* #see MilestoneRepositoryInterface
*/
public function findCurrent()
{
$now = new DateTime;
if ($this->current === false) {
$this->current = $this->createQueryBuilder('m')
->where('m.startsAt <= :now')
->andWhere('(m.endsAt IS NULL OR :now < m.endsAt)')
->setParameter('now', $now->format('Y-m-d'))
->orderBy('m.startsAt', 'ASC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult()
;
}
return $this->current;
}
/**
* #see MilestoneRepositoryInterface
*/
public function isOpen()
{
$current = $this->findCurrent();
return $current && $current->isStart();
}
}
And here is my EntityRepository.php file:
namespace Common\Doctrine\ORM;
use Doctrine\ORM\EntityRepository as BaseEntityRepository;
use Doctrine\ORM\QueryBuilder;
class EntityRepository extends BaseEntityRepository
{
protected $alias = 'x';
public function add($entity)
{
$em = $this->getEntityManager();
$em->persist($entity);
$em->flush();
}
public function remove($entity)
{
$em = $this->getEntityManager();
$em->remove($entity);
$em->flush();
}
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null, $result = true)
{
$alias = $this->alias;
$builder = $this->createQueryBuilder($alias);
$this->applyCriteria($builder, $alias, $criteria);
$this->applyOrderBy($builder, $alias, $orderBy);
$this->applyLimit($builder, $limit);
$this->applyOffset($builder, $offset);
if (!$result) {
return $builder;
}
return $builder->getQuery()->getResult();
}
public function findOneBy(array $criteria, array $orderBy = null, $result = true)
{
$alias = $this->alias;
$builder = $this->createQueryBuilder($alias);
$this->applyCriteria($builder, $alias, $criteria);
$this->applyOrderBy($builder, $alias, $orderBy);
if (!$result) {
return $builder;
}
return $builder->getQuery()->getOneOrNullResult();
}
protected function applyCriteria(QueryBuilder $builder, $alias, array $criteria)
{
$map = $this->getCriteriaMap();
foreach ($criteria as $property => $value) {
if (array_key_exists($property, $map)) {
call_user_func_array($map[$property], [$builder, $alias, $property, $value]);
} else {
$this->applyDefaultCriterion($builder, $alias, $property, $value);
}
}
}
protected function getCriteriaMap()
{
return [];
}
protected function applyDefaultCriterion($builder, $alias, $property, $value)
{
if (null === $value) {
$builder->andWhere($alias.'.'.$property.' IS NULL');
} else {
$parameter = 'p_' . uniqid();
$builder->andWhere($alias.'.'.$property.' = :'.$parameter);
$builder->setParameter($parameter, $value);
}
}
/**
* Apply order by
*
* #param QueryBuilder $builder
* #param string $alias
* #param array|null $orderBy
* #return void
*/
protected function applyOrderBy(QueryBuilder $builder, $alias, array $orderBy = null)
{
if (empty($orderBy)) {
$orderBy = $this->getDefaultOrder();
}
$map = $this->getOrderingMap();
foreach ($orderBy as $property => $direction) {
if (array_key_exists($property, $map)) {
call_user_func_array($map[$property], [$builder, $alias, $property, $direction]);
} else {
$this->applyDefaultOrder($builder, $alias, $property, $direction);
}
}
}
protected function getDefaultOrder()
{
return [];
}
protected function getOrderingMap()
{
return [];
}
protected function applyDefaultOrder(QueryBuilder $builder, $alias, $property, $direction)
{
$builder->orderBy($alias.'.'.$property, $direction);
}
protected function applyLimit(QueryBuilder $builder, $limit = null)
{
if ($limit) {
$builder->setMaxResults($limit);
}
}
protected function applyOffset(QueryBuilder $builder, $offset = null)
{
if ($offset) {
$builder->setFirstResult($offset);
}
}
}
I think I'm accessing through a service, with this:
services:
# Factories
milestones.factory.milestone:
alias: milestones.repository.milestone
arguments: [ MilestonesBundle\Entity\Milestone ]
# Repositories
milestones.repository.milestone:
class: MilestonesBundle\Entity\Repository\MilestoneRepository
factory_service: doctrine.orm.default_entity_manager
factory_method: getRepository
arguments: [ MilestonesBundle\Entity\Milestone ]
replace this code:
milestones.repository.milestone:
class: MilestonesBundle\Entity\Repository\MilestoneRepository
factory_service: doctrine.orm.default_entity_manager
factory_method: getRepository
arguments: [ MilestonesBundle\Entity\Milestone ]
with this one:
milestones.repository.milestone:
class: MilestonesBundle\Entity\Repository\MilestoneRepository
factory: ['#doctrine.orm.entity_manager', getRepository]
arguments: [ MilestonesBundle\Entity\Milestone ]
factory method - getRepository
I think somewhere in jour code this method is called:
public function create()
{
$class = $this->getClassName();
return new $class;
}
And this is calling the constructor of Doctrine\ORM\EntityRepository:
public function __construct(EntityManagerInterface $em, Mapping\ClassMetadata $class)
{
$this->_entityName = $class->name;
$this->_em = $em;
$this->_class = $class;
}
therefore you will have to inject the arguments if you want to use your create method... I think it should be something like new $class($entityManager, Entity::class)
I have to entities Project and Image. They are linked by OneToOne relation.
Here is the property definition in the project:
/**
* #Exclude
* #ORM\OneToOne(targetEntity="SensoBundle\Entity\Image", cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $image;
Here is the image entity
<?php
namespace SensoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Image
*
* #ORM\Table(name="image")
* #ORM\HasLifecycleCallbacks()
* #ORM\Entity(repositoryClass="SensoBundle\Repository\ImageRepository")
*/
class Image
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="fileName", type="string", length=255)
*/
private $fileName;
/**
* #var string
*
* #ORM\Column(name="parentType", type="string", length=255, nullable=true)
*/
private $parentType;
private $tempFilename;
private $file;
private $fileNamePrefix;
private $fileExtension;
private $hashForName;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set fileName
*
* #param string $fileName
* #return Image
*/
public function setFileName($fileName)
{
$this->fileName = $fileName;
return $this;
}
/**
* Get fileName
*
* #return string
*/
public function getFileName()
{
return $this->fileName;
}
public function getFile()
{
return $this->file;
}
public function setFile($file = null)
{
$this->file = $file;
if (null !== $this->fileName) {
$this->tempFilename = $this->fileName;
$this->fileName = null;
}
}
public function getUploadDir()
{
return '/uploads/img';
}
public function getUploadRootDir()
{
return __DIR__ . '/../../../web' . $this->getUploadDir();
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null === $this->file) {
return;
}
if (null !== $this->fileName) {
$this->tempFilename = $this->fileName;
}
$this->fileExtension = $this->file->guessExtension();
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
if (null !== $this->tempFilename) {
$oldFile = $this->getUploadRootDir() . '/' . $this->tempFilename;
if (file_exists($oldFile)) {
unlink($oldFile);
}
}
$this->file->move(
$this->getUploadRootDir(),
$this->fileName
);
}
/**
* #ORM\PreRemove()
*/
public function preRemoveUpload()
{
$this->tempFilename = $this->getUploadRootDir() . '/' . $this->fileName;
}
/**
* #ORM\PostRemove()
*/
public function removeUplaod()
{
if (file_exists($this->tempFilename)) {
unlink($this->tempFilename);
}
}
public function setFileNamePrefix($prefix)
{
$this->fileNamePrefix = $prefix;
}
public function setHashForname($hash)
{
$this->hashForName = $hash;
}
/**
* Set parentType
*
* #param string $parentType
* #return Image
*/
public function setParentType($parentType)
{
$this->parentType = $parentType;
return $this;
}
/**
* Get parentType
*
* #return string
*/
public function getParentType()
{
return $this->parentType;
}
public function getFileExtension() {
return $this->fileExtension;
}
public function getFilenamePrefix() {
return $this->fileNamePrefix;
}
}
Here is the part of controller which is concerned :
/**
* #Route("/project/{id}/presentation/edit", name="project_presentation_edit")
* #Method({"GET", "POST"})
*/
public function editPresentationAction(Request $request, $id)
{
$entity = $this->get('creasenso.project_repo')->find($id);
if (!$entity) {
throw $this->createNotFoundException('The project does not exist');
}
$this->denyAccessUnlessGranted('editProject', $entity);
$form = $this->createForm('CreasensoBundle\Form\Project\ProjectPresentationType', $entity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
$this->get('session')->getFlashBag()->add('success', 'project.presentation.feedback');
$redirectRoute = ($request->request->get('nextSubmit')) ? 'project_details_edit' : 'project_presentation_edit';
return $this->redirectToRoute($redirectRoute, array('id' => $id));
}
return $this->render('CreasensoBundle:project:editPresentation.html.twig', array(
'form' => $form->createView(),
'entity' => $entity
));
}
Here is the form called in the controller above
<?php
// src/Infinite/InvoiceBundle/Form/Type/InvoiceType.php
namespace CreasensoBundle\Form\Project;
use Infinite\FormBundle\Form\Type\PolyCollectionType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectPresentationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('image', 'SensoBundle\Form\Image\ImageSimpleEditType', array(
'label' => 'Cover',
'attr' => array(
'help_text' => 'project.cover.help'
),
'display_image_format' => 'projectCoverSmall',
'imagesConstraints' => array(
'minWidth' => 300
)
))
->add('trans_name_fr', 'SensoBundle\Form\Type\FieldTranslationType', array(
'mapped' => false,
'cascade_validation' => true,
'fieldName' => 'name',
'fieldLocale' => 'fr',
'fieldOptions' => array(
'label' => 'French name',
'required' => true
)
))
->add('trans_name', 'SensoBundle\Form\Type\FieldTranslationType', array(
'mapped' => false,
'cascade_validation' => true,
'fieldName' => 'name',
'fieldLocale' => 'en',
'fieldOptions' => array(
'label' => 'English name',
'required' => true
)
))
;
}
public function getBlockPrefix()
{
return 'projectContent';
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'CreasensoBundle\Entity\Project'
));
}
}
Here is my generic Image Entity Field
<?php
namespace SensoBundle\Form\Image;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\Image as ImageValidator;
use SensoBundle\Entity\Image;
class ImageSimpleEditType extends AbstractType
{
private $imageUrl = null;
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::POST_SET_DATA, function ($event) use ($builder, $options) {
$form = $event->getForm();
$imageFieldParams = array(
'label' => false,
'display_image_format' => $options['display_image_format'],
'required' => false
);
if (!empty($options['imagesConstraints'])) {
// Image format
$constraints = $options['imagesConstraints'];
$validatorParams = array();
if (!empty($constraints['format'])) {
switch ($constraints['format']) {
case 'square':
$squareErrMsg = "image.edit.squareError";
$validatorParams['allowSquare'] = true;
$validatorParams['allowLandscape'] = false;
$validatorParams['allowPortrait'] = false;
$validatorParams['allowLandscapeMessage'] = $squareErrMsg;
$validatorParams['allowPortraitMessage'] = $squareErrMsg;
break;
case 'landscape':
$landscapeErrMsg = "image.edit.landscapeError";
$validatorParams['allowSquare'] = false;
$validatorParams['allowLandscape'] = true;
$validatorParams['allowPortrait'] = false;
$validatorParams['allowLandscapeMessage'] = $landscapeErrMsg;
$validatorParams['allowPortraitMessage'] = $landscapeErrMsg;
break;
case 'portrait':
$portraitErrMsg = "image.edit.portraitError";
$validatorParams['allowSquare'] = false;
$validatorParams['allowLandscape'] = false;
$validatorParams['allowPortrait'] = true;
$validatorParams['allowLandscapeMessage'] = $portraitErrMsg;
$validatorParams['allowPortraitMessage'] = $portraitErrMsg;
break;
}
// Min width
if (!empty($constraints['minWidth'])) {
$validatorParams['minWidth'] = $constraints['minWidth'];
}
}
$imageFieldParams['constraints'] = new ImageValidator($validatorParams);
}
$form->add('file', 'SensoBundle\Form\Image\ImageInputType', $imageFieldParams);
});
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'SensoBundle\Entity\Image',
'display_image_format' => 'original',
'imagesConstraints' => null
));
}
public function getBlockPrefix()
{
return 'sensoImage';
}
}
And finally, here is my generic image file input (used in the image entity form)
<?php
namespace SensoBundle\Form\Image;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use SensoBundle\Entity\Image;
use Symfony\Component\Validator\Constraints\Image as ImageValidator;
class ImageInputType extends AbstractType
{
private $imageManager;
public function __construct($imageManager)
{
$this->imageManager = $imageManager;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
//
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['image_url'] = null;
$imageEntity = $form->getParent()->getData();
if ($imageEntity) {
$this->imageManager->setEntity($imageEntity);
$view->vars['image_url'] = $this->imageManager->getRelativeUrl();
}
$view->vars['display_image_format'] = $options['display_image_format'];
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'display_image_format' => 'original',
'format' => null
));
}
public function getBlockPrefix()
{
return 'sensoImageInput';
}
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\FileType';
}
}
My form structure is like that
-- Project form (class ProjectPresentationType)
---- Image entity field (class ImageSimpleEditType)
------ Image input (class ImageInputType)
My problem is an image entity is created even if no file is uploaded.
What I would like is to be able to create a new project with this "features":
Image upload is not required
If image field is empty, don't create an Image entity
Thank you for your help.
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 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.