I know that similar questions have been asked here on SO, but answers don't satisfy me.
Having following:
Standard form with fields foo, bar, baz, bat, x
DTO (not entity, dummy object, no annotations)
Constraints on some fields attached using FormBuilder
Form is used in multiple places.
In some places that form is used, I would like to add single validator to fields foo, bar, baz. This validator can get only values of those fields or whole propagated DTO. It should have access to DI Container to call service that will check data against database.
For now I'm thinking about one of two solutions:
Adding extra constraint to the form in controllers that require it (sound dirty)
Adding extra field to form constructor/DTO (not boolean but business logic that tells if extra validation is needed) and adding extra constraint to form.
Problem is that I can't figure out how to handle either of those cases.
Finally, I want to emphasize that I don't want to use validation groups and annotations - both will add extra dependencies and logic to DTO.
Found the answer. For short answer you can check out This Matt Daum post.
Here's full example, presenting not only how to create custom form validator, but also how to inject services and extra data to form (because that was my case).
If you want simple receipe, go straight to the bottom.
Let's have DTO:
class MyFormDTO
{
/** #var string */
private $name;
/** #var string */
private $surname;
/** #var string */
private $phone;
/** getters and setters ommited */
}
Now, define dependencies in form. First two are services, last one (Calendar) is some extra data needed from controller.
class MyForm extends AbstractType
{
(fields hidden)
/**
* #param Sender $sender
* #param TranslatorInterface $translator
* #param Calendar $calendar
*/
public function __construct(Sender $sender, TranslatorInterface $translator, Calendar $calendar)
{
$this->translator = $translator;
$this->sender = $sender;
$this->calendar = $calendar;
}
}
Now there are two ways - if you only need services in your form, you can just define your form as a service. If you, like me, need extra data, you need to write form factory service:
class MyFormFactory
{
(fields hidden)
/**
* #param Sender $sender
* #param TranslatorInterface $translator
*/
public function __construct(Sender $sender, TranslatorInterface $translator)
{
$this->sender = $sender;
$this->translator = $translator;
}
/**
* #param Calendar $calendar
*
* #return MyForm
*/
public function getMyForm(Calendar $calendar)
{
return new MyForm($this->sender, $this->translator, $calendar);
}
}
Let's define this factory as a service with right dependencies:
mybundle.form.myform_factory:
class: MyBundle\Service\FormFactory\MyFormFactory
arguments: [ #text_message.sender, #translator ]
How to get the form in controller? Easy as that:
class MyController extends Controller
{
/**
* #ParamConverter("calendar", options={"mapping"={"calendarId":"id"}})
*
* #param Request $request
* #param Calendar $calendar
*
* #return Response
* #throws Exception
*/
public function myAction(Request $request, Calendar $calendar)
{
$formDTO = new MyFormDto();
$myForm = $this->get('mybundle.form.myform_factory')->getMyForm($calendar);
$form = $this->createForm($myForm, $formDTO);
(handling post hidden)
}
}
And now the most important part - we have services injected properly into our form. How to use them and validate selected data? like this:
class MyForm extends AbstractType
{
(fields hidden, constructor shown in previous example)
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** #var MyFormDTO $myDTO */
$myDTO = $options['data'];
(build form as usual, using services and data from $options and $this->calendar injected by controller and factory)
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => true,
'constraints' => [
new Callback(function (MyFormDTO $data, ExecutionContextInterface $context) //notice that we have access to fully propageted DTO here
{
//use injected service
$isValid = $this->sender->validateSomething($data->getSurname(), $data->getPhone());
if (false === $isValid)
{
$context
->buildViolation($this->translator->trans('wrong_surname_phone_pair'))
->addViolation();
}
return $isValid;
})
],
]);
}
}
Related
I'm using Symfony 3 to build a website. I have an Entity (Users) that is in OneToOne relation with itself in order to make couples. (I didn't have others idea on how to do it easily)
The end goal is to create a form to reference the id of the other Users in the couple. So I created an IntegerType field and assign it the id but I can't set it (because there are no setId(...)). So I would know if there is a setter option (can't find in Doc/Tests), and if there isn't how could I achieve this ?
The steps to register a new couple would have been:
Send new id (of the other Users) [FORM]
Fetch the other Users ($userCouple = ...findOne...) [BDD]
If he have $couple == null then $userCouple->setCouple($this) and $this->setCouple($userCouple)
So my Users entity looks like:
<?php
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
/**
* Users
*
* #ORM\Table(name="users")
* #ORM\Entity(repositoryClass="Acme\UserBundle\Repository\UsersRepository")
*/
class Users extends BaseUser
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Acme\UserBundle\Entity\Users")
* #ORM\JoinColumn(nullable=true)
*/
protected $couple;
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set couple.
*
* #param \Acme\UserBundle\Entity\Users|null $couple
*
* #return Users
*/
public function setCouple(\Acme\UserBundle\Entity\Users $couple = null)
{
$this->couple = $couple;
return $this;
}
/**
* Get couple.
*
* #return \Acme\UserBundle\Entity\Users|null
*/
public function getCouple()
{
return $this->couple;
}
}
And my form looks like :
<?php
namespace Acme\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
class ProfileFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('couple', IntegerType::class, array(
'label' => 'Couple ID',
'property_path' => 'couple.id',
'attr' => array('min' => 0),
));
}
public function getBlockPrefix()
{
return 'acme_user_profile';
}
}
You should solve this using a one-to-one self-referencing relation (see more here). Basically your couple would be replaced by partner which suites best the case:
...
/**
* #OneToOne(targetEntity="User")
* #JoinColumn(name="partner_id", referencedColumnName="id")
*/
protected $partner;
...
Then in the form you could use the EntityType (not sure why you wanted to use IntegerType in the first place) and do something like this:
$builder->add('users', EntityType::class, array(
// query choices from this entity
'class' => 'UserBundle:User',
// use the User.username property as the visible option string
'choice_label' => 'username',
));
Of course you can exclude the user you're editing the profile from the list of users you show as possible partners using query_builder option (passing a custom query) or choices to pass the collection of User entities you want to use (getting them first and filter out current user).
In a controller of a Symfony2 application, I use collection of elements. The more elements there are, the more time it takes. It sounds logical but it seems that there are repeated traitments:
I create a form from MyCollectionType
in the buildForm method, I add the ElementType that has its own buildForm method.
Then when the form is built by Symfony2 with the list of elements I pass, the buildForm method of the ElementType is called one time for each element
=> is this not possible that the first ELementType is built, then other are cloned?
I don't see why there would be any different there between these subforms, the only difference will appear setting the data and not building the form.
Then I notice the same for the buildView method: there are a lot of repeated processing for each element, where only the data (possibly processing of listeners) may vary.
For example, in my application, with a ElementType having 6 fields, and a collection of 700 elements, it takes up to 30s to render the form.
Is it due to the way that forms are handled, or can be be optimized ?
I add the same problem for one of my application, what I did is that I reimplemented a basic form class prototype, the method names are almost the same that the Symfony2 ones and reimplemented in the most simple way, It looks like this:
class SimpleForm
{
/**
* #var ContainerInterface $container Main application DIC
*/
protected $container;
/**
* #var array $options Options of the form
*/
protected $options = array();
/**
* #var array $options Raw datas for the form.
*/
protected $data = array();
/**
* #var array $options Data of the form against user input.
*/
protected $boundData = array();
/**
* #var array $errors List of errors after form valisation
*/
protected $errors = array();
/**
* #var array $options Tells if the datas were bound to the form.
*/
protected $isBound = false;
/**
* #var array $options Tells if the form validation is ok
*/
protected $isValid = false;
/**
* Main constructor.
*
* #param ContainerInterface $container The main DIC
* #param array $options Options of the form
*/
public function __construct(ContainerInterface $container, $options = array())
{
$this->container = $container;
$this->options = $options;
$this->buildForm();
}
/**
* Add widgets to the form.
*/
public function buildForm()
{
$this->widgets['keywordType'] = self::$keywordTypes;
}
/**
* #return array
*/
public function getOptions()
{
return $this->options;
}
/**
* #param array $options
*/
public function setOptions($options)
{
$this->options = $options;
}
/**
* #return string
*/
public function getEnctype()
{
return isset($this->options['enctype']) ? $this->options['enctype'] : '';
}
/**
* If the form is bound it return the bound datas, it returns the raws datas otherwise.
*
* #return array
*/
public function getData()
{
return $this->isBound ? $this->boundData : $this->data;
}
/**
* #throws \LogicException
*
* #return array
*/
public function getErrors()
{
if ($this->isBound == false) {
throw new \LogicException('The form must be bound before the errors can be retrieved');
}
return $this->errors;
}
/**
* #throws \LogicException
*
* #return array
*/
public function hasErrors()
{
if ($this->isBound == false) {
throw new \LogicException('The form must be bound before the errors can be checked');
}
return !empty($this->errors);
}
/**
* #throws \LogicException
*
* #return array
*/
public function getBoundData()
{
if ($this->isBound == false) {
throw new \LogicException('The form must be bound before getting the form final datas');
}
return $this->boundData;
}
/**
* #param array $data
*
* #return SimpleForm
*/
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* Bind the submitted values to the form.
*
* #param array $values The values to bind
*
* #return SimpleForm
*/
public function bind($values)
{
$values = $this->clean($values);
$this->boundData = Tools::arrayDeepMerge($this->data, $values);
$this->isBound = true;
$this->validate();
return $this;
}
/**
* Clean raw form values before the validation.
*
* #param array $values The raw values submitted by the user.
*
* #return array
*/
protected function clean($values)
{
// ...
return $values;
}
/**
* Run the validation against the form.
*
* #return boolean
*/
protected function validate()
{
$this->errors = array();
// ...
$this->isValid = empty($this->errors);
return $this->isValid;
}
/**
* Return the validation state of the form.
*
* #throws \LogicException
*
* #return boolean
*/
public function isValid()
{
if ($this->isBound == false) {
throw new \LogicException('The form must be bound before getting the validation status');
}
return $this->isValid;
}
/**
* Returns the datas that will be necesary for the view.
*
* #return array
*/
public function createView()
{
return array(
'widgets' => $this->widgets,
'options' => $this->options,
'data' => $this->boundData, // Don't forget to escape values against XSS
'enctype' => $this->getEnctype(),
'errors' => $this->errors,
'name' => $this->getName(),
);
}
/**
* #return string The name of the form
*/
public function getName()
{
return 'search';
}
}
In the Twig templates, I just iterate the data to create the form fields, repopulate values and to display the errors. The form is the same but is generated in 100ms instead of 30 seconds... For all the other forms I kept on using the Symfony2 ones.
The answer is simple: You should optimize yours programming skills instead of symfony forms. This is yours next question (of many) about the same issue.
You want to render over 4200 fields with fieldsets and/or divs around each input, so I suspect that 30 second it's the time in which it renders in the browser.
Has anyone encountered this problem when using doctrine embeddables and symfony forms?
if you don't know Doctrine Embeddables are, you can read about it here
http://doctrine-orm.readthedocs.org/en/latest/tutorials/embeddables.html
When using value object (CategoryType in my case) with symfony form on form submission (during persisting to DB) I get the following warning
Warning: ReflectionProperty::getValue() expects parameter 1 to be object, string given
This happens because symfony form returns string instead of embeddable object.
The only workaround I have right now is to use mapped => false on type field and create valid embeddable object inside controller action just before persist. But that's far from "nice" solution and I want to avoid that.
My Entity, Value Object and form (simplified for the sake of question)
Foo\BarBundle\Entity\CategoryType
<?php
namespace Foo\BarBundle\Entity;
class CategoryType
{
/**
* #var string
*/
protected $value;
public function __toString()
{
return (string) $this->getValue();
}
/**
* #return string
*/
public function getValue()
{
return $this->value;
}
/**
* #param string $value
*
* #return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
}
Foo\BarBundle\Resources\config\doctrine\CategoryType.orm.yml
CategoryType.orm.yml
Foo\BarBundle\Entity\CategoryType:
type: embeddable
fields:
value:
type: string
Foo\BarBundle\Entity\Category
<?php
namespace Foo\BarBundle\Entity;
class Category
{
/**
* #var integer
*/
protected $id;
/**
* #var string
*/
protected $name;
/**
* #var CategoryType
*/
protected $type;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #return \Foo\BarBundle\Entity\CategoryType
*/
public function getType()
{
return $this->type;
}
/**
* #param \Foo\BarBundle\Entity\CategoryType $type
*
* #return $this
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* #return string
*/
public function __toString()
{
return (string) $this->getName();
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #param string $name
*
* #return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
}
Foo\BarBundle\Resources\config\doctrine\Category.orm.yml
Foo\BarBundle\Entity\Category:
type: entity
fields:
id:
type: integer
id: true
generator:
strategy: AUTO
name:
type: string
embedded:
type:
class: CategoryType
Foo\BarBundle\Form\CategoryType
<?php
namespace Foo\BarBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CategoryType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('type')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Foo\BarBundle\Entity\Category'
)
);
}
/**
* #return string
*/
public function getName()
{
return 'category_form';
}
}
The doctrine embedabble behavior is just a persistence mechanism, so, it can not break the form component which is independant from it (and works with all objects graph).
The issue is your form is not well designed (it does not follow your object graph). Here, you have a category which wraps a category type. So, your form must follow the same structure at the definition level.
You must create a CategoryTypeType form (mapped to your CategoryType class) where your add a value field. Then, in your CategoryType (the form one), you must embed the CategoryTypeType for the type field.
Then, the form component will automatically creates a Category which wraps a CategoryType. Then, Doctrine will simply persists your object as embeddable and everything will work :)
I'd like to use one text form field to save data in two entity properties. However I didn't found any solution/hint which might lead to a clean implementation.
Consider the following example†:
There is a Doctrine entity with two properties: count, unit
A user enters "34 apples" in a simple text input field
The DataTransformer (or whatever is appropriate) should transform this value in such a way that in the underlying entity count = '34' and unit = 'apple' is being saved.
you could have a field which is not mapped in the database and in the prePersist and preUpdate methods you would transform the data and save these in the appropriate fields. For example:
/**
*
* #ORM\HasLifecycleCallbacks()
*/
class MyClass
{
private $info;
/**
* #ORM\Column(name="count", type="integer")
*/
private $count;
/**
* #ORM\Column(name="unit", type="string")
*/
private $unit;
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function separateInfo(){
$infoArray = explode(' ', $this->info);
$this->count = $infoArray[0];
$this->unit = $infoArray[1];
}
}
And in the form you only add the 'info' field:
class MyClassType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('info');
}
}
This is not tested, but it's the idea which could help you.
I hope this be useful for you.
Kind regards.
I'm writing a feature which calls for the records of my joining table to carry extra metadata (Joining-Table with Metadata). I've attempted to implement this in accordance with this section of the Doctrine documentation.
See below for example Entity definitions.
The challenge now is that getGroups and setGroups do not yield/set Group entities (& the same is true from the Group instance perspective), but they yield GroupUser entities.
This adds a substantial delay to process of managing this relationships, which so far have been extremely smooth - for example, I cannot simply add, remove, or check for the existence of a Group to the collection which getGroups yields.
Can anyone identity any errors in my implementation, or else recommend a more fluid way of implementing this concept?
Thanks in advance for any input.
EDIT:
My main concern is this: using this implementation, retrieving a collection of Users from a Group entity requires this Entity method's mediation:
public function getUsers() {
return $this->users->map(function($groupUser){
return $groupUser->getUser();
});
}
I'm concerned that this could imply a major performance hit down the road. Am I incorrect?
Furthermore, how does one re-implement the setUsers method?
Group entity:
<?php
/**
* #Entity
* #Table(name="group")
*/
class Group {
/**
* #Column(type="integer", nullable=false)
* #Id
*/
protected $id = null;
/**
* #OneToMany(targetEntity="GroupUser", mappedBy="group")
* #var \Doctrine\Common\Collections\Collection
*/
protected $users;
}
User entity:
<?php
/**
* #Entity
* #Table(name="user")
*/
class User {
/**
* #Column(type="integer", nullable=false)
* #Id
*/
protected $id = null;
/**
* #OneToMany(targetEntity="GroupUser", mappedBy="user")
* #var \Doctrine\Common\Collections\Collection
*/
protected $groups;
}
Joining entity:
<?php
/**
* #Entity
* #Table(name="group_user")
*/
class GroupUser {
/**
* #Id
* #ManyToOne(targetEntity="User", inversedBy="groups")
* #JoinColumn(name="userId", referencedColumnName="id")
*/
protected $user;
/**
* #Id
* #ManyToOne(targetEntity="Group", inversedBy="users")
* #JoinColumn(name="groupId", referencedColumnName="id")
*/
protected $group;
/**
* #Column(type="integer")
*/
protected $relationship;
}
Related -
Same goal, slightly different approach, which consistently produced errors once the resulting collections were manipulated: http://www.doctrine-project.org/jira/browse/DDC-1323
Supports the approach, no technical details: Doctrine 2 join table + extra fields
I've found just two examples (see question) of entity definitions for this specific type of relationship, however no example code for how they're used. As such it was fairly unclear how fluid (or otherwise) the resulting setters & getters could be expected to be. Hopefully this code will help clear up the approach for anyone else making a similar attempt.
The ideal solution under the circumstances (thanks #doctrine # freenode) was to implement a custom repository - a more flexible & efficient place for creating & managing the association.
Example Custom Repository for Join-Table with Metadata Class - Solution accompanies code in original question
<?php
use Doctrine\ORM\EntityRepository;
class GroupUserRepository extends EntityRepository {
/**
* #param \User $user
* #param \Group $group
* #param integer $type One of the integer class constants defined by GroupUser
* #param string $role Optional string defining user's role in the group.
* #return \GroupUser
*/
public function addUserToGroup(User $user, Group $group, $relationship, $role = '') {
$groupUser = $this->findOneBy(array('user' => $user->getId(), 'group' => $group->getId()));
if(!$groupUser) {
$groupUser = new GroupUser();
$groupUser->setGroup($group);
$groupUser->setUser($user);
$groupUser->setRole($role);
$groupUser->setRelationship($relationship);
$this->_em->persist($groupUser);
}
return $groupUser;
}
/**
* #param \User $user
* #param \Group $group
* #return null
*/
public function removeUserFromGroup(User $user, Group $group) {
$groupUser = $this->findOneBy(array('user' => $user->getId(), 'group' => $group->getId()));
if($groupUser)
$this->_em->remove($groupUser);
}
}
Then, from the join-table class, modify the Entity meta-data accordingly to specify the custom repository.
<?php
/**
* #Entity(repositoryClass="\Path\To\GroupUserRepository")
*/
class GroupUser {
// ...
}
This causes the custom repository to yield in place of the default one, making a proxy method from the Entity class simple.
<?php
/**
* #Entity
*/
class Group {
/**
* #param \User $user
* #param integer $relationship One of the integer class constants defined by GroupUser
* #param string $role Optional string defining user's role in the group.
* #return \GroupUser
*/
public function addUser(User $user, $relationship, $role = '') {
return $this->_em->getRepository('GroupUser')
->addUserToGroup($user, $this, $relationship, $role);
}
}
And things are about as manageable as they were before.