I am working on a REST webservice (FOSRestBundle 2.0.0, Symfony 3.1.3) and testing the creation of entities. The creation itself works fine with a correct set of data but if I try to omit a required value the controller still says the form is valid.
The entity itself:
class Customer implements ExportableEntity
{
use Traits\FilterableTrait;
use Traits\UuidTrait;
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Serializer\Exclude()
* #Serializer\ReadOnly()
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="customer_index", type="integer", unique=true)
*/
private $customerIndex;
/**
* #var string
*
* #ORM\Column(name="customerName", type="string", length=255)
*/
private $customerName;
// [... accessors ...]
The controller:
/**
* #ApiDoc(
* resource=false,
* description="Create a new customer",
* section="Customers",
* statusCode={
* 200="Action successful",
* 403="Authorization required but incorrect / missing information or unsufficient rights",
* 500="Returned if action failed for unknown reasons"
* }
* )
*
* #param Customer $customer
* #return RestResponse
*/
public function postCustomerAction(Request $request) {
$manager = $this->container->get('corebundle.managers.customer');
// Internal usage only, no link with the WS issue
$manager->setChecksEnabled(false);
$customer = new Customer();
$form = $this->get('form.factory')->createNamed(null, CustomerType::class, $customer, ['csrf_protection' => false]);
$form->handleRequest($request);
//if ($form->isValid()) {
if ($form->isSubmitted() && $form->isValid()) {
print('VALID');
exit();
$manager->create($customer);
// Return 201 + Location
}
return \FOS\RestBundle\View\View::create($form, 400);
}
And the FormType:
class CustomerType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('customerName', Type\TextType::class, array('label' => 'Customer name'))
->add('customerIndex', Type\IntegerType::class, array('label' => 'Customer Index'))
->add('comment', Type\TextareaType::class, array('label' => 'Comments',
'required' => false, ))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'NetDev\CoreBundle\Entity\Customer'));
}
/**
* #return string
*/
public function getBlockPrefix()
{
return 'netdev_corebundle_customer';
}
}
If I try to create a new Customer and omit the "customerIndex" field, I belieev that I should get an invalid form error but I ain't getting it.
I tried to change the "handleRequest" with
$form->submit([])
and
$form->submit($request->request->get($form->getName()))
to no avail. If I add a "NotBlank()" constraint to the entity itself it works but I am under the impression that this would be a workaround, not a fix. Did I miss something ?
$form->isValid()
This line will verify that your submitted data respected all the constraints written in your entity files (with Assert annotation, for example #Assert\NotBlank()).
So, you did not miss something.
Related
i use Symfony3.3 and want to make a form for the admin administration. The user should be have a group and the group sould have the roles for the backend access.
The form for groups (name and roles) i finished and the form for the admins (name, passwort...) is finish too.
The admin will be find and have the group. If i load the admin it have the arraycollection with the groups.
Here my classes
admin:
class Admin extends BaseUser
{
/**
* #ORM\Id()
* #ORM\Column(name="idAdmin", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=255, options={"default":NULL})
*/
protected $style;
/**
* #ORM\ManyToMany(targetEntity="AdminBundle\Entity\AdminGroup")
* #ORM\JoinTable(
* name="admin_has_group",
* joinColumns={
* #ORM\JoinColumn(name="idAdmin", referencedColumnName="idAdmin")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="idGroup", referencedColumnName="idGroup")
* }
* )
*/
protected $groups;
/**
* #return string
*/
public function getStyle()
{
return $this->style;
}
/**
* #param string $style
*/
public function setStyle($style)
{
$this->style = $style;
return $this;
}
public function setGroups($groups)
{
$this->groups = $groups;
return $this;
}
public function getGroups()
{
return $this->groups;
}
}
groups
class AdminGroup extends BaseGroup
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="idGroup", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Group constructor.
*
* #param string $name
* #param array $roles
*/
public function __construct($name = '', $roles = array())
{
parent::__construct($name, $roles);
}
}
form generation
$admin = $this->getDoctrine()->getRepository(Admin::class)->find(1);
$admingroupList = $this->getDoctrine()->getRepository(AdminGroup::class)->findAll();
$form = $this->createFormBuilder($admin)
->add("username", TextType::class)
->add('plainPassword', PasswordType::class, $passwordSettings)
->add(
'groups', ChoiceType::class, [
'required' => false,
'multiple' => true,
'choices' => $admingroupList,
])
->add('save', SubmitType::class, ['label' => 'Save'])->getForm()->createView();
save form
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$em->persist($user);
$em->flush();
....
}
The first problem is that i have in the overview only display the id's in the select.
The second problem is that i submit the form (with selected groups) symfony crashed with the following message
Call to a member function contains() on array
I try to convert the grouplist to an normal array they will be crashed at the save the data
Expected argument of type "FOS\UserBundle\Model\GroupInterface", "integer" given
I dont know that i sould do to make a simple symfony form with the admin data and group selection... i dont find any example for a form with fosuserbundle...
Have someone an idea what i can to without manipulate the fosuserbundle entites or the symfonycode?
If you need more source, tell me with part :)
Editing 10.12.17
I try to convert the ChoosenArray into this format
$list = [
'user' => 0,
'admin' => 1
];
but than it will be broken at
$form->handleRequest($request);
with the error:
Expected argument of type "FOS\UserBundle\Model\GroupInterface", "integer" given
I do not think the data returned from
$admingroupList = $this->getDoctrine()->getRepository(AdminGroup::class)->findAll();
Will work as you want it too. choices wants something like this
'choices' => [
'Admin' => 'admin',
'User' => 'user'
]
Where the key of the array is the name the user sees, and the value of the array is the value used in the <option>.
You probably need to manipulate the $admingroupList array to mimic the demo array above. Or write your own query in the AdminGroup Repo to return a pre-formatted array for use with a Symfony form.
In Sonata I have created several lists and all work fine. (Please not that that was a while ago, so I may have done something there which fixed the issue I will describe here...).
Now I have created a new listing of Playlist entities:
As you can see in the picture, both the "Id" column and the "Aangemaakt op" columns are sortable, however the "Playlist" column is not.
Both the "Aangemaakt op" and the "Playlist" fields are date-fields, but since the "Aangemaakt op" field is sortable I would say that has nothing to do with it.
I have been searching the Sonata documentation, Google and StackOverflow, but haven't found any clue concerning this issue. I did find thread about sorting a list based on an Entity field, but my field isn't an entity.
Relevant code:
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper) {
$listMapper
->add('id')
->add('playlist_date', 'date', array('label' => 'Playlist'))
->add('created', 'datetime', array('label' => 'Aangemaakt op'))
->add(
'_action', 'actions', array(
'actions' => array(
'delete' => array(),
)
)
);
}
Some StackOverflow threads and an answer below mention adding 'sortable' => true to the field that must be sortable.
Doing that indeed makes the column clickable to sort, but clicking it results in the following exception:
Catchable Fatal Error: Argument 1 passed to
Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery::entityJoin()
must be of the type array, null given, called in
/path/of/my/project/sonata-project/doctrine-orm-admin-bundle/Datagrid/ProxyQuery.php
on line 142 and defined.
According to other StackOverflow threads that is because a join must be created. However, the field is simply a field of the same Mysql record as the other fields. I did find a StackOverflow thread mentioning this as well and in which they joined the same record in order to make this work, but I didn't get that to work. Besides, I thank that shouldn't be the way to order the contents of a column.
Does anyone have a clue?
Update in reaction to Hibatallah Aouadni's answer
As Hibatallah suggests, I added the following to my PlaylistsAdmin:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('playlist_date')
->add('created');
}
This resulted in the following error message:
Notice: Undefined index: playlist_date
So I inspected my Entity and I found that it has a UniqueConstraint:
uniqueConstraints={#ORM\UniqueConstraint(name="playlist_date", columns={"playlist_date"})}
It does not have an actual "index" defined, but ofcourse it is. However as a test I added the following:
, indexes={#ORM\Index(name="playlist_date", columns={"playlist_date"})}
This didn't give any different result.
So still no luck at all :(
** Entity and Entity admin **
Entity Admin:
<?php
namespace Company\AdminBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\AdminBundle\Route\RouteCollection;
class PlaylistsAdmin extends Admin
{
protected $baseRoutePattern = 'playlists';
protected $baseRouteName = 'playlists';
protected function configureRoutes(RouteCollection $collection) {
$collection->clearExcept(array('list', 'delete', 'show'));
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper) {
$listMapper
->add('id')
->add('playlist_date', 'date', array('label' => 'Playlist'))
->add('created', 'datetime', array('label' => 'Aangemaakt op'))
->add(
'_action', 'actions', array(
'actions' => array(
'show' => array(),
/*'edit' => array(),*/
'delete' => array(),
)
)
);
}
public function getBatchActions() {
return array();
}
}
Entity:
<?php
namespace Company\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Playlist
*
* #ORM\Table(name="playlist", uniqueConstraints={#ORM\UniqueConstraint(name="playlist_date", columns={"playlist_date"})})
* #ORM\Entity()
*/
class Playlist
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", options={"unsigned"=true})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var \DateTime
*
* #ORM\Column(name="playlist_date", type="date", nullable=true)
*/
protected $playlistDate;
/**
* #var \DateTime
*
* #ORM\Column(name="created", type="datetime", nullable=true)
*/
protected $created;
/**
* #var \Doctrine\Common\Collections\Collection
*/
protected $video;
/**
* Constructor
*/
public function __construct() {
$this->video = new \Doctrine\Common\Collections\ArrayCollection();
$this->setCreated(new \DateTime());
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set playlistDate
*
* #param \DateTime $playlistDate
* #return Playlist
*/
public function setPlaylistDate($playlistDate) {
$this->playlistDate = $playlistDate;
return $this;
}
/**
* Get playlistDate
*
* #return \DateTime
*/
public function getPlaylistDate() {
return $this->playlistDate;
}
/**
* Set created
*
* #param \DateTime $created
* #return Playlist
*/
public function setCreated($created) {
$this->created = $created;
return $this;
}
/**
* Get created
*
* #return \DateTime
*/
public function getCreated() {
return $this->created;
}
/**
* Add video
*
* #param \Company\AppBundle\Entity\Video $video
* #return Playlist
*/
public function addVideo(\Company\AppBundle\Entity\Video $video) {
$this->video[] = $video;
return $this;
}
/**
* Remove video
*
* #param \Company\AppBundle\Entity\Video $video
*/
public function removeVideo(\Company\AppBundle\Entity\Video $video) {
$this->video->removeElement($video);
}
/**
* Get video
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getVideo() {
return $this->video;
}
}
Finally I found it, it's so absurd, in the configureListFields method, you have to call the attribute with its name not the DataBase name, so:
change
->add('playlist_date', 'date', array('label' => 'Playlist'))
to
->add('playlistDate', 'date', array('label' => 'Playlist'))
:D I can't beleive we spend all this time for some absurd mistake ;)
all you need to do is to add the argument sortable to array and true as value:
->add('playlist_date', 'date', array(
'label' => 'Playlist',
'sortable' => true
))
try to add the playlist field in configureDatagridFilters in your entity admin:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('playlist_date')
->add('created');
}
and it will work ;)
I am building an app based on Sylius standard edition. With the ResourceBundle i handled to integrate my own entities and the corresponding relations. This new resources should be related later to the products entity, but first i want to get it working "standalone". The backend works for both, the added resource and for relations. These are editable via form-collections. Very fine! Now i want to get translated database-content for my new resource. I tried the way i did it in Symfony earlier, but it didn't work. The last vew days i tried every possible solution found, but none of this works, or i made mistakes... Neither the translation-tables where constructed when typing:
app/console doctrine:schema:update --force
nor translatable content is visible in the forms. When calling the edit action, i get following error:
error:
Neither the property "translations" nor one of the methods "getTranslations()", "translations()", "isTranslations()", "hasTranslations()", "__get()" exist and have public access in class "KontaktBundle\Entity\Kontakte".
Is somebody out there with an example implementation of a extended Sylius-resource with translateable database-content? I'm still learning symfony and sylius too, can you tell me what i'm missing or doing wrong?
Many Thanks to #gvf. Now i figured out that the config entry must be set. This gave me a functional example which i want to provide here:
Configs
# app/config/sylius_config.yml (must be imported in config)
# Adding Resource
sylius_resource:
# Resource Settings
settings:
sortable: true
paginate: 50
allowed_paginate: [50, 100, 500]
filterable: true
resources:
dbk.authors:
driver: doctrine/orm
templates: AuthorBundle:Backend
object_manager: default
classes:
model: AuthorBundle\Entity\Kontakte
#interface: // if you have an interface configured
controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
repository: Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository
translation:
model: AuthorBundle\Entity\AuthorsTranslation
mapping:
fields: ['anrede','biografie']
Services
# app/config/sylius_services.yml (must be imported in config)
parameters:
# Parameter for our author entity
app.form.type.authors.class: AuthorBundle\Form\AuthorsType
app.form.type.authors_translation.class: AuthorBundle\Form\AuthorsTranslationType
services:
# Adding Authors Backend menu Item
dbk_backend_authors.menu_builder:
class: AuthorBundle\EventListener\MenuBuilderListener
tags:
- { name: kernel.event_listener, event: sylius.menu_builder.backend.main, method: addBackendMenuItems }
- { name: kernel.event_listener, event: sylius.menu_builder.backend.sidebar, method: addBackendMenuItems }
# Adding Authors FormType
app.form.type.authors:
class: "%app.form.type.authors.class%"
tags:
- {name: form.type, alias: dbk_authors }
app.form.type.authors_translation:
class: "%app.form.type.authors_translation.class%"
tags:
- {name: form.type, alias: dbk_authors_translation }
EventListener (Adds Menu Entry in Sylius-Backend)
<?php
// AuthorBundle/EventListener/MenuBuilderListener/MenuBuilderListener.php
namespace AuthorBundle\EventListener;
use Sylius\Bundle\WebBundle\Event\MenuBuilderEvent;
class MenuBuilderListener
{
public function addBackendMenuItems(MenuBuilderEvent $event)
{
$menu = $event->getMenu();
$menu['assortment']->addChild('vendor', array(
'route' => 'Authors',
'labelAttributes' => array('icon' => 'glyphicon glyphicon-user'),
))->setLabel('Authors');
}
}
Entities
Authors (The new resource hich we want to add and to translate)
<?php
// AuthorBundle/Entity/Authors.php
namespace AuthorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Translation\Model\AbstractTranslatable;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Authors
*
* #ORM\Entity
* #ORM\Table(name="authors")
*/
class Authors extends AbstractTranslatable
{
//
// IDENTIFIER FIELDS
//
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", length=20, nullable=false, options={"unsigned":"true"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
//
// FIELDS
//
/**
* #var string
*
* #ORM\Column(name="vorname", type="string", length=255)
*/
private $vorname;
/**
* #var string
*
* #ORM\Column(name="nachname", type="string", length=255)
*/
private $nachname;
public function __construct() {
parent::__construct();
}
//
// TranslationFields - Getters and Setters
//
/**
* Get Anrede
* #return string
*/
public function getAnrede()
{
return $this->translate()->getAnrede();
}
/**
* Set Anrede
*
* #param string $anrede
*
* #return Authors
*/
public function setAnrede($anrede)
{
$this->translate()->setAnrede($anrede);
return $this;
}
/**
* Get Biografie
* #return string
*/
public function getBiografie()
{
return $this->translate()->getBiografie();
}
/**
* Set Biografie
*
* #param string $biografie
*
* #return Authors
*/
public function setBiografie($biografie)
{
$this->translate()->setBiografie($biografie);
return $this;
}
//
// Getters and Setters
//
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set vorname
*
* #param string $vorname
*
* #return Authors
*/
public function setVorname($vorname)
{
$this->vorname = $vorname;
return $this;
}
/**
* Get vorname
*
* #return string
*/
public function getVorname()
{
return $this->vorname;
}
/**
* Set nachname
*
* #param string $nachname
*
* #return Authors
*/
public function setNachname($nachname)
{
$this->nachname = $nachname;
return $this;
}
/**
* Get nachname
*
* #return string
*/
public function getNachname()
{
return $this->nachname;
}
public function __toString(){
return $this->getFullName();
}
}
AuthorsTranslation (This is the Entity for tanslation of Authors)
<?php
// AuthorBundle/Entity/AuthorsTranslation.php
namespace AuthorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Translation\Model\AbstractTranslation;
/**
* AuthorsTranslation
*
* #ORM\Entity
* #ORM\Table(name="authors_translation")
*/
class AuthorsTranslation extends AbstractTranslation
{
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", length=20, nullable=false, options={"unsigned":"true"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
//
// TRANSLATABLE - FIELDS
//
/**
* #var string
* #ORM\Column(name="anrede", type="string", length=255)
*/
private $anrede;
/**
* #var string
* #ORM\Column(name="biografie", type="text")
*/
private $biografie;
/**
* {#inheritdoc}
*/
public function getId()
{
return $this->id;
}
//
// GETTERS AND SETTERS
//
/**
* Set anrede
*
* #param string $anrede
* #return Authors
*/
public function setAnrede($anrede)
{
$this->anrede = $anrede;
return $this;
}
/**
* Get anrede
*
* #return string
*/
public function getAnrede()
{
return $this->anrede;
}
/**
* Set biografie
*
* #param string $biografie
*
* #return Authors
*/
public function setBiografie($biografie)
{
$this->biografie = $biografie;
return $this;
}
/**
* Get biografie
*
* #return string
*/
public function getBiografie()
{
return $this->biografie;
}
}
FormTypes
AuthorsType
<?php
// AuthorBundle/Form/AuthorsType.php
namespace AuthorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\QueryBuilder;
class AuthorsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Add Translations to Form.
->add('translations', 'a2lix_translations', array(
'required' => false,
'fields' => array(
'anrede' => array(
'label' => 'Anrede',
),
'biografie' => array(
'label' => 'Biografie',
'attr' => array('data-edit' => 'wysiwyg', 'rows' => '15'),
'required' => false,
)
)
))
->add('vorname', null, array(
'label' => 'Vorname',
'required' => false,
))
->add('nachname', null, array(
'label' => 'Nachname',
'required' => false,
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'data_class' => 'AuthorBundle\Entity\Authors'
));
}
/**
* #return string
*/
public function getName()
{
return 'dbk_authors';
}
}
AuthorsTranslationType
<?php
// AuthorBundle/Form/AuthorsTranslationType.php
namespace AuthorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class AuthorsTranslationType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('anrede', 'text', array(
'label' => 'Anrede'
))
->add('biografie', 'textarea', array(
'label' => 'Biografie'
))
;
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'dbk_authors_translation';
}
}
Form Template
{# AuthorBundle/Resources/views/backend/edit||add.html.twig - as you like... the call of form.translation (line 5) is the point here #}
{% form_theme form 'SyliusWebBundle:Backend:forms.html.twig' %}
<div class="row">
<div class="col-md-12">
{{ form_row(form.translations, {'attr': {'class': 'input-lg'}}) }}
{{ form_row(form.vorname) }}
{{ form_row(form.nachname) }}
</div>
</div>
Have a look at the Product and ProductTranslation under the models folder in Sylius Components to see an example of how Sylius implements it.
Kontakte needs to extend AbstractTranslatable, you also need to create a class KontakteTranslation that extends AbstractTranslation. Under sylius_resource you also need to configure the translations:
sylius_resource:
resources:
dbk.contact:
driver: doctrine/orm
templates: KontaktBundle:Backend
object_manager: default
classes:
model: KontaktBundle\Entity\Kontakte
controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
repository: Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository
translation:
model: KontaktBundle\Entity\KontakteTranslation
mapping:
fields: {...fields that will be translated...}
Get rid of gedmo translatable extension because Sylius doesn't use it.
I have a form for inserting an entity Category. This entity has two other entities that are related to it.
One related entity is an other separate Entity Group. The other entity is itself self-referenced Category that is an array collection that represents preconditions. So far so good, i can persist the main entity with the relations with the correct ORM annotations.
Rough scheme of Category
id : int
title: string
group : Group obj
preconditions : [Category obj, Category obj, ...]
I made an type class for creating the form as described as best-practice in the documentation.
$form = $this->createForm(new CategoryType($em));
Situation
Before i persist the entity, i must initialize it and set the posted datas to it. The posted related objects can’t simply setted to the persisting entity, because they have the wrong datatype. (E.g. the self-referencing collection is posted only as array with id’s, and not an array collection of the choosed items.)
So i catch this raw datas and get separatelly the related entities from the entity manager.
Goal
The inserting entity should be filled automatically with the related entities, whitout get those separately through the entity manager
Question
Is this the meaning of the form component that those related objects are not posted and made available fully? Or what im missing in my implementation?
Is there a way to do this more automated?
On the form class for the ‘preconditions’ property i had to do mapped => false otherwise i recieve an exception that a wrong type was passed. But at the end i want that the form matches all automatically through the mapping, whitout skipping a mapping, and whitout getting the related entities separately from the entity manager.
class CategoryType extends AbstractType
{
public function __construct($em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$qb = $this->em->createQueryBuilder();
$categories = $qb->select('e.id, e.title')
->from('MyvendorCoreBundle:Category', 'e')
->indexBy('e', 'e.id')
->orderBy('e.title')
->getQuery()
->getResult();
$categories_choice = array_map(function ($value) {
return $value['title'];
}, $categories);
$builder->add('title')
->add('group_Id', new GroupType($this->em))
->add('preconditions', 'choice', array(
'choices' => $categories_choice,
'multiple' => true,
'mapped' => false
))
->add('save', 'submit');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Myvendor\CoreBundle\Entity\Category'
));
}
public function getName()
{
return 'category';
}
}
Controller method
public function newAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(new CategoryType($em));
// Repopulating the form after submission
$form->handleRequest($request);
// Prepare a new empty Category
$category = new Category();
if ($form->isValid()) {
/* Catch some raw datas posted from the form */
// Posted precondition category ids to get its entities more later
$precondition_category_ids = $form->get('preconditions')->getData();
// Posted group entity that have only filled the group id in the object
$group_raw = $form->get('group_Id')->getData();
// Get the explicit filled group entity throuth the posted id.
$group = $em->find('MyvendorCoreBundle:Group', $group_raw->getGroupid());
// Fill the prepaired group with the posted datas
$category->setTitle($form->get('title')->getData());
$category->setGroupId($group);
// Adding preconditions
try {
for ($i = 0; count($precondition_category_ids) > $i; $i ++) {
$precondition_category_id = $precondition_category_ids[$i];
if (0 >= $precondition_category_id) { // Retrieving id must be greater than 0
throw new \Exception('Error retrieving precondition id');
}
$precondition_category = $em->find('MyvendorCoreBundle:Category', $precondition_category_id);
if ($precondition_category instanceof Category) {
$category->addPrecondition($precondition_category);
} else {
throw new \Exception('Error retrieving precondition as Myvendor\CoreBundle\Entity\Category');
}
}
$em->persist($category); // Insert the group item with its relations
$em->flush();
echo '<h1 style="color:green">persisted</h1>';
} catch (\Exception $e) {
echo '<h1 style="color:red">' . $e->getMessage() . '</h1>';
}
}
return $this->render('MyvendorCoreBundle:fbm:new.html.twig', array(
'form' => $form->createView()
));
}
GroupType
class GroupType extends AbstractType
{
public function __construct($em){
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$groups = $this->em->createQuery("
SELECT o.groupid, o.descr
FROM MyvendorCoreBundle:Group o
INDEX BY o.groupid
ORDER BY o.descr
")->getResult();
$groups_dropdown = array();
$groups_dropdown = array_map(function($value) { return $value['descr']; }, $groups);
$builder->add('groupid', 'choice', array(
'label' => false,
'choices' => $groups_dropdown,
'attr' => array('style' => 'width: 300px')
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Myvendor\CoreBundle\Entity\Group',
));
}
public function getName()
{
return 'group';
}
}
/**
* #ORM\Entity
* #ORM\Table(name="category")
*/
class Category
{
public function __construct()
{
$this->preconditions = new ArrayCollection();
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var \Myvendor\CoreBundle\Entity\Group
*
* #Assert\Type(type="Myvendor\CoreBundle\Entity\Group")
* #Assert\Valid()
* #ORM\ManyToOne(targetEntity="Myvendor\CoreBundle\Entity\Group", inversedBy="Category")
* #ORM\JoinColumn(name="group_id", nullable=false, referencedColumnName="groupid")
*/
private $group_Id;
/**
* #var string
* #Assert\NotBlank()
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $title;
/**
* Preconditions are Categorys referencing to an Category.
* For a single Category its empty (which have no subelements).
* A join table holds the references of a main Category to its sub-Categorys (preconditions)
*
* #ORM\ManyToMany(targetEntity="Category")
* #ORM\JoinTable(name="category_precondition",
* joinColumns={#JoinColumn(name="category_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="category_precondition_id", referencedColumnName="id")}
* )
*/
private $preconditions;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*
* #return Category
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set groupId
*
* #param \Myvendor\CoreBundle\Entity\Group $groupId
*
* #return Category
*/
public function setGroupId(\Myvendor\CoreBundle\Entity\Group $groupId)
{
$this->group_Id = $groupId;
return $this;
}
/**
* Get groupId
*
* #return \Myvendor\CoreBundle\Entity\Group
*/
public function getGroupId()
{
return $this->group_Id;
}
/**
* Add precondition
*
* #param \Myvendor\CoreBundle\Entity\Category $precondition
*
* #return $this
*/
public function addPrecondition(\Myvendor\CoreBundle\Entity\Category $precondition)
{
$this->preconditions[] = $precondition;
return $this;
}
/**
* Get preconditions
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPreconditions()
{
return $this->preconditions;
}
/**
* Group
*
* #ORM\Table(name="group", indexes={#ORM\Index(name="homepage", columns={"homepage"}), #ORM\Index(name="theme", columns={"theme"})})
* #ORM\Entity
*/
class Group
{
/**
* #var string
*
* #ORM\Column(name="descr", type="string", length=60, nullable=true)
*/
private $descr;
/**
* #var integer
*
* #Assert\NotBlank()
* #ORM\Column(name="groupid", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
public $groupid;
/**
* Set descr
*
* #param string $descr
* #return Group
*/
public function setDescr($descr)
{
$this->descr = $descr;
return $this;
}
/**
* Get descr
*
* #return string
*/
public function getDescr()
{
return $this->descr;
}
/**
* Get groupid
*
* #return integer
*/
public function getGroupid()
{
return $this->groupid;
}
}
The solution was that the type of the selecting choice entities, must be not a choicelist, but really an collection type.
So use something like this
->add('preconditions', 'collection', array(
'entry_type' => 'entity',
'entry_options' => array(
'class' => 'MyVendorCoreBundle:EduStructItem',
'choice_label' => 'title'
),
'allow_add' => true,
'allow_delete' => true
))
instead of
->add('preconditions', 'choice', array(
'choices' => $categories_choice,
'multiple' => true,
'mapped' => false
))
I've created a custom form validator that does not seem to be working. It gets called during form submission and returns false when it should fail validation. But it doesn't seem to tell the form that validation has failed.
Here is the form validator: (code box scrolls)
<?php
namespace Redacted\AppBundle\Validator\Constraints;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RequiredIfPlainPasswordSetValidator extends ConstraintValidator
{
/**
* RequestStack instance.
*
* #var Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* dependency injection.
*
* #param Symfony\Component\HttpFoundation\RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* if the plain password has been set, the current password must not be
* empty.
*
* #param string $currentPassword
* #param Symfony\Component\Validator\Constraint $constraint
*
* #return bool
*/
public function validate($currentPassword, Constraint $constraint)
{
$plainPassword = $this->requestStack->getCurrentRequest()->request
->get('security_settings')['plainPassword']['first'];
if ($plainPassword && !$currentPassword) {
return false;
}
return true;
}
}
And here is the constraint that goes with it:
<?php
namespace Redacted\AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class RequiredIfPlainPasswordSet extends Constraint
{
/**
* error message
*
* #var string
*/
protected $message = 'Please enter your current password.';
/**
* the alias for the related validator in services.yml
*
* #return string
*/
public function validatedBy()
{
return 'required_if_plain_password_set';
}
}
and the relevant part of app/config/services.yml:
services:
redacted.appbundle.required_if_plain_password_set:
class: Redacted\AppBundle\Validator\Constraints\RequiredIfPlainPasswordSetValidator
arguments: ["#request_stack"]
tags:
- { name: validator.constraint_validator, alias: required_if_plain_password_set }
I've created a custom form type: (code box scrolls)
<?php
namespace Redacted\AppBundle\Form;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
class SecuritySettingsFormType extends AbstractType
{
/**
* the name of the form type
*
* #return string
*/
public function getName()
{
return 'security_settings';
}
/**
* add fields to form
*
* #param Symfony\Component\Form\FormBuilderInterface $formBuilder
* #param array $options
*/
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$formBuilder
->add('email')
->add('currentPassword', 'password')
->add('plainPassword', 'repeated', [
'type' => 'password',
'invalid_message' => 'Passwords must match.',
]);
}
/**
* configureOptions.
*
* #param Symfony\Component\OptionsResolver\OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$options = [
'data_class' => 'AppBundle\Entity\User',
'validation_groups' => ['security_settings'],
];
$resolver->setDefaults($options);
}
}
which validates against the User entity due to the data_type. Here is the relevant part of the User entity:
<?php
namespace Redacted\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Redacted\AppBundle\Validator\Constraints\RequiredIfPlainPasswordSet;
class User implements UserInterface, \Serializable
{
// ...
/**
* #var string (default: null)
*
* #Assert\NotBlank(message="user.email_required", groups={"security_settings"})
* #Assert\Email(message="security_settings.valid_email" groups={"security_settings"})
* #ORM\Column(name="email", type="string", length=255, nullable=true, unique=true)
*/
private $email = null;
/**
* current password - used for validation only
*
* #RequiredIfPlainPasswordSet(
* message="security_settings.current_password_required",
* groups={"security_settings"}
* )
* #var string
*/
protected $currentPassword;
// ... setters and getters for above, etc.
}
And finally, here's the controller method I'm using to check. I'll leave out the view because it's probably irrelevant. (I'm defining my controller as a service so no redirectToRoute(), etc.)
// in the controller class...
/**
* Security settings page - email, password, etc.
*
* #Route("/security-settings", name="security_settings")
* #Template()
*
* #param Symfony\Component\HttpFoundation\Request $request
*
* #return array|Symfony\Component\HttpFoundation\RedirectResponse
*/
public function securitySettingsAction(Request $request)
{
$loggedInUser = $this->tokenStorage->getToken()->getUser();
$form = $this->formFactory
->create(new SecuritySettingsFormType(), $loggedInUser);
$form->handleRequest($request);
if ($form->isValid()) {
$user = $form->getData();
// persist the user
$this->saveUserSettings($user);
// set a success message
$this->setSecuritySettingsFlashSuccess($request);
// redirect back
$route = $this->router->generate('security_settings');
return new RedirectResponse($route);
}
return ['form' => $form->createView()];
}
The idea is that the current password should only be required if the new password is entered. Although that validate() function is getting called and returning false when it should, the form's isValid() is returning true and it's saving. If I add a #Assert\NotBlank(groups={"security_settings"}) assertion to the User::currentPassword field, it does fire and fail successfully, so it is looking for validation annotations on that field.
What am I missing?
The problem was that the validate() method of the ConstraintValidator should not just return true or false, it should build a violation like so:
/**
* if the plain password has been set, the current password must not be
* empty.
*
* #param string $currentPassword
* #param Symfony\Component\Validator\Constraint $constraint
*/
public function validate($currentPassword, Constraint $constraint)
{
$plainPassword = $this->requestStack->getCurrentRequest()->request
->get('security_settings')['plainPassword']['first'];
if ($plainPassword && !$currentPassword) {
$this->context->buildViolation($constraint->message)
->atPath('currentPassword')->addViolation();
}
}
Thanks to my coworker for catching it. All works as expected now.