I have a FormType method.
<?php
class PostType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('text')
->add('paymentMethod', 'choice', array(
'required' => true,
'choices' => array(
'cach' => 'Cach',
'check'=> 'Check'
)
))
->add('checkId', 'integer', array(
'required' => true
))
->add('submit', 'submit');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Post',
]);
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_post';
}
}
?>
The field checkId is displaying only when the choice 'check' is selected with the field paymentMethod.
Here is the javascript:
PaymentMethod = {
hideCheckId: function(){
$('.check-id').hide();
},
showCheckId: function(){
$('.check-id').fadeIn();
},
whenPaymentMethodChange: function(){
$('#appbundle_post_paymentMethod').on('change', function(){
var method = this.value ;
if(method == 'check'){
PaymentMethod.showCheckId();
}else{
PaymentMethod.hideCheckId();
}
})
}
};
PaymentMethod.hideCheckId();
PaymentMethod.whenPaymentMethodChange();
I would like the field 'checkId' to be required to TRUE only when the option check is selected.
How can I do that?
Ok thanks, Here is the solution for everyone:
Add a callback method function in your entity class.
/**
* #param ExecutionContextInterface $context
* #Assert\Callback()
*/
public function isPaymentIsCheck(ExecutionContextInterface $context)
{
if ($this->getPaymentMethod() == 'check' and $this->getCheckId() == '') {
$context->buildViolation('A check ID as to be defined')
->atPath('checkID')
->addViolation();
}
}
Don't forget to add the Assert and ExecutionContextInterface component in you entity class:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
And finally don't forget to display errors in the twig template:
{% if form.vars.errors|length %}
<div class="alert alert-danger">{{ form_errors(form) }}</div>
{% endif %}
Hope it will help you.
More info here: http://symfony.com/doc/current/reference/constraints/Callback.html
What KevinR wrote would definitely solve your problem, but in my opinion there's even cleaner solution:
http://symfony.com/doc/current/book/validation.html#group-sequence-providers
Imagine a User entity which can be a normal
user or a premium user. When it's a premium user, some extra
constraints should be added to the user entity (e.g. the credit card
details). To dynamically determine which groups should be activated,
you can create a Group Sequence Provider. First, create the entity and
a new constraint group called Premium:
This is exactly your how you would solve this kinda of problem
public function getGroupSequence()
{
$groups = array('Post'); //Or array('Default') whichever you prefer
if ($this->getPaymentMethod() === 'Check') {
$groups[] = 'Check';
}
return $groups;
}
Of course you'll have to add validation group paymentMethod property in your entity mapping. Here's annotation example, for more examples check above link.
/**
* #Assert\IsTrue(groups={"Check"})
*/
private $checkId;
Related
Is it possible to apply validation constraints to a Symfony login form?
To me it looks like that won't work. I use Symfony 5.2
I have created a Symfony login form as described on the documentation page "https://symfony.com/doc/current/security/form_login_setup.html".
Now I would like to validate the form and have created the following constraints for this in "validation.yaml".
App \ Entity \ User:
properties:
username:
- NotBlank:
message: 'form.user.username.not_blank'
password:
- NotBlank:
message: 'form.user.password.not_blank'
Unfortunately the constraints are ignored.
If I leave the username and password fields blank, I get a message that the login failed. In such a case, I would like to receive the constraint message that the username and password cannot be empty.
I didn't get any further during my research on the Internet.
Could it be that no validation constraints can be used in a Symfony login form?
Has anyone of you successfully set up validation constraints in a Symfony 5 login form and can you give me a tip on what to look out for?
I stumbled upon a similar issue - and used the following solution:
Since the authentification happens before the regular form validation I implemented a custom validation in the 'authenticate' method of my LoginFormAuthenticator:
public function authenticate(Request $request): PassportInterface
{
$credentials = $request->get('login');
<snip>
$errors = $this->validateCredentials($credentials);
if (0 !== $errors->count()) {
throw new AuthenticationException();
}
return new Passport(
new UserBadge($credentials['email']),
new PasswordCredentials($credentials['password']),
[
new CsrfTokenBadge('login_token', $credentials['_csrf_token']),
new RememberMeBadge(),
]
);
}
The validateCredentials method which stores the $error-object in the session:
public function validateCredentials($credentials) {
$constraints = new Assert\Collection([
'fields' => [
'email' =>
new Assert\Sequentially([
new Assert\NotBlank([
'message' => 'login.email.not_blank'
]),
new Assert\Email([
'message' => 'login.email'
])
]),
<snip>
],
'allowExtraFields' => true
]);
$errors = $this->validator->validate(
$credentials,
$constraints
);
if (0 !== $errors->count()) {
$this->session->set('login-errors', $errors);
} else {
$this->session->remove('login-errors');
}
return $errors;
}
The SecurityController fetches the $error-object from the session and adds the respective errors to the login form:
$loginForm = $this->createForm(LoginType::class, $formData);
$loginErrors = $request->getSession()->get('login-errors');
if ($loginErrors) {
foreach ($loginErrors as $error) {
$propertyPath = trim($error->getPropertyPath(), '[]');
$errorMessage = $error->getMessage();
$loginForm->get($propertyPath)->addError(new FormError($errorMessage));
}
}
Most likely not the best approach - but it does the job reasonably well and it's only the login form that makes this extra validation step necessary.
With model
You have to add these constraints to the entitiy (if you are using it).
I'd suggest to use the annotations:
https://symfony.com/doc/current/validation.html#configuration
Just add those to your entity.
In Your Model (Entity):
use Symfony\Component\Validator\Constraints as Assert;
class MyClass
{
/**
* #Assert\NotBlank
*/
private $myVarToBeAsserted;
}
Without model
If you are using a data class (no model behind the form), you can use annotations as well. But in this case you have to add those to your form itself:
https://symfony.com/doc/current/form/without_class.html
MyFormType:
$builder->add(
'birth',
DateType::class,
[
'required' => true,
'constraints' =>
[
new NotBlank(),
new Date()
],
'label' => $labelBirth,
'widget' => 'single_text',
'html5' => true,
'attr' =>
[
],
]
);
... and in controller:
$form = $this->createForm(
MyFormType::class,
[
'data_class' => true
],
);
What you did btw: You defined the message for those possible assertions. Which would be shown if your assertion NotBlank() would be triggered.
thank you for your answer to my question.
I have just tested it with annotations, but unfortunately it doesn't work for me even with that. When I submit the "empty" login form, I get the message "Invalid credentials.".
I don't understand why Symfony is checking the login data here even though the form fields are empty.
Before the access data can be validated, it must first be checked whether the form has been filled out correctly.
Here is some sample code to illustrate what I'm trying to do.
User.php
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
/**
* #return string|null
*/
public function getUsername(): ?string
{
return $this->username;
}
/**
* #param string|null $username
* #return $this
*/
public function setUsername(?string $username): self
{
$this->username = $username;
return $this;
}
/**
* #return string|null
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* #param string|null $password
* #return $this
*/
public function setPassword(?string $password): self
{
$this->password = $password;
return $this;
}
/**
* #return array
*/
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* #param array $roles
* #return $this
*/
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #return string|null
*/
public function getSalt() :?string
{
return null;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
}
}
LoginType.php
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\User;
use Gregwar\CaptchaBundle\Type\CaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LoginType extends AbstractType
{
public const CAPTCHA_REQUIRED = 'captcha_required';
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username', TextType::class, ['label' => 'login_form.username'])
->add('password', PasswordType::class, ['label' => 'login_form.password'])
->add('submit', SubmitType::class, ['label' => 'button.login']);
if ($options[self::CAPTCHA_REQUIRED]) {
$builder
->add('captcha', CaptchaType::class, ['label' => 'login_form.captcha']);
}
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
self::CAPTCHA_REQUIRED => false,
]);
}
}
validator.yaml
framework:
validation:
enabled: true
email_validation_mode: html5
validation.yaml
App\Entity\User:
properties:
username:
- NotBlank: ~
password:
- NotBlank: ~
Incidentally, I have no problems with any of the other forms.
Only especially with my login form does the validation of the constraints not work.
I suspect because Symfony first validates the access data instead of checking the constraints first.
Have you ever successfully used constraints in a Syfmony login form?
thank you for your proposed solution.
Your suggestion helped me a lot.
I have now implemented the validation in my login form in this way.
In my LoginFormAuthenticator class I added a new method "validate". This method validates the login form and saves the errors in the session.
private function validate(array $credentials)
{
$user = new User();
$user->setUsername($credentials['username'])
->setPassword($credentials['password']);
$errors = $this->validator->validate($user);
if (0 !== $errors->count()) {
$this->session->set(SessionKey::LOGIN_VALIDATION_ERRORS, $errors);
} else {
$this->session->remove(SessionKey::LOGIN_VALIDATION_ERRORS);
}
}
In my SecurityController class, I check whether validation errors are stored in the session. If there are any, I will add them to the login form as you have already described in your post.
$loginErrors = $request->getSession()->get(SessionKey::LOGIN_VALIDATION_ERRORS);
if ($loginErrors) {
foreach ($loginErrors as $error) {
$propertyPath = trim($error->getPropertyPath(), '[]');
$errorMessage = $error->getMessage();
$form->get($propertyPath)->addError(new FormError($errorMessage));
}
}
For me this is a workable solution. Maybe not nice but it works.
This is how I did Symfony 4.4 login form server-side validation. It must be done before Symfony performs its built-in authentication checks: in my AppCustomAuthenticator I added a local validator in the getCredentials() method:
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
// I need to validate the login form fields before the actual authentication
// do NOT validate with entity annotations, but with local validator (otherwise, the User entity validations would trigger errors like it would for create/update User)
$dataConstraints = new Assert\Collection([
'fields' => [
'username' => [
new Assert\NotBlank([
'message' => 'Username cannot be empty'
]),
new Assert\Length([
'min' => 4,
'max' => 100,
'minMessage' => 'Username is too short. It should have 4 characters or more.',
'maxMessage' => 'Username is too long. It should have 100 characters or less.'
])
],
'password' => [
new Assert\NotBlank([
'message' => 'Password cannot be empty'
]),
new Assert\Length([
'min' => 4,
'max' => 100,
'minMessage' => 'Password is too short. It should have 4 characters or more.',
'maxMessage' => 'Password is too long. It should have 100 characters or less.'
])
]
],
// to allow more fields in the first parameter of $this->validator->validate() [which is the array of fields to validate], but which have no constraint in the second parameter (in $dataConstraints)
'allowExtraFields' => true
]);
$errors = $this->validator->validate(
$credentials,
$dataConstraints
);
if (0 !== $errors->count()) {
$arrErrors = [];
foreach($errors as $error){
$arrErrors[] = $error->getMessage();
}
throw new CustomUserMessageAuthenticationException('Login failed.',$arrErrors);
}
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
So, do not use User entity validator for the login form, because entity Assert constraints are for create/update forms (register user/edit user). Then, in my controller, login route:
/**
* #Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils)
{
// if already loggedin, redirect to another page
// if ($this->getUser()) {
// return $this->redirectToRoute('target_path');
// }
// here we prepare the errors for the view, if any
// I made it two-dimensional array, you can do whatever
$retError = [];
// get the login error(s)
$errors = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
// prepare the Login form errors (including the Symfony authentication error; $errors includes any of them, from the exceptions in AppCustomAuthenticator), if any
if($errors){
$retError[] = [
'error_group' => $errors->getMessageKey(),
'error_items' => $errors->getMessageData()
];
}
return $this->render('site/login.html.php', [
'last_username' => $lastUsername,
'retError' => $retError
]);
}
In my login.html.php view (PHP templating):
<?php if ($retError) { ?>
<div class="alert alert-danger">
<ul>
<?php foreach($retError as $error){ ?>
<li>
<?=$error['error_group'];?>
<?php
if(is_array($error['error_items']) && count($error['error_items']) > 0){
?>
<ul>
<?php foreach($error['error_items'] as $error_item){ ?>
<li><?=$error_item;?></li>
<?php } ?>
</ul>
<?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
I have a ManyToMany relation in my Symfony 4.2.6 application and I would like for it to be possible to have this to be null.
So my first entity SpecialOffers is as follows :
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\SpecialOfferRepository")
*/
class SpecialOffer
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Neighbourhood", inversedBy="specialOffers")
*/
private $neighbourhood;
public function __construct()
{
$this->neighbourhood = new ArrayCollection();
}
/**
* #return Collection|Neighbourhood[]
*/
public function getNeighbourhood(): Collection
{
return $this->neighbourhood;
}
public function addNeighbourhood(Neighbourhood $neighbourhood): self
{
if (!$this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood[] = $neighbourhood;
}
return $this;
}
public function removeNeighbourhood(Neighbourhood $neighbourhood): self
{
if ($this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood->removeElement($neighbourhood);
}
return $this;
}
}
It is related to the neighbourhood class :
/**
* #ORM\Entity(repositoryClass="App\Repository\NeighbourhoodRepository")
*/
class Neighbourhood implements ResourceInterface
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\SpecialOffer", mappedBy="neighbourhood")
*/
private $specialOffers;
public function __construct()
{
$this->specialOffers = new ArrayCollection();
}
/**
* #return Collection|SpecialOffer[]
*/
public function getSpecialOffers(): Collection
{
return $this->specialOffers;
}
public function addSpecialOffer(SpecialOffer $specialOffer): self
{
if (!$this->specialOffers->contains($specialOffer)) {
$this->specialOffers[] = $specialOffer;
$specialOffer->addNeighbourhood($this);
}
return $this;
}
public function removeSpecialOffer(SpecialOffer $specialOffer): self
{
if ($this->specialOffers->contains($specialOffer)) {
$this->specialOffers->removeElement($specialOffer);
$specialOffer->removeNeighbourhood($this);
}
return $this;
}
}
And finally the form is
class SpecialOfferType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'placeholder' => 'form.neighbourhood.all'
]
);
}
}
But when I don't select a specific neighbourhood for the Special offer in my form I get the following error :
Could not determine access type for property "neighbourhood" in class "App\Entity\SpecialOffer": The property "neighbourhood" in class "App\Entity\SpecialOffer" can be defined with the methods "addNeighbourhood()", "removeNeighbourhood()" but the new value must be an array or an instance of \Traversable, "NULL" given.
Is there anyway I can make it so that my special offer either contains and array of neighbourhoods or just null ?
I feel like I'm overlooking something really obvious, any help would be greatly appreciated
Test =>
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'multiple' => true,
'placeholder' => 'form.neighbourhood.all'
]
);
Since your fields on the entities are both many-to-many, thus expecting an array (or similar) and the form field is of EntityType, which will return one Entity of the expected type or null, I feel like there is some form of asymmetry.
I would consider using the CollectionType from the start or at least setting the multiple option on the form to true, so that the return value is an array.
Another option would be to add a DataTransformer to the form field, which turns null into an empty array and one entity into an array of one entity, and vice-versa.
Installed easyadminbundle with symfony 4, configured for an entity name Delivery and it has a field associated to another entity name WeeklyMenu:
easy_amin.yaml:
Delivery:
...
form:
fields:
- { property: 'delivered'}
- { property: 'weeklyMenu', type: 'choice', type_options: { choices: null }}
I need a dynamically filtered results of weeklyMenu entity here, so I can get a list of the next days menus and so on. It's set to null now but have to get a filtered result here.
I've read about overriding the AdminController which I stucked with it. I believe that I have to override easyadmin's query builder that listing an associated entity's result.
i've figured out, here is the solution if someone looking for:
namespace App\Controller;
use Doctrine\ORM\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilder;
class AdminController extends EasyAdminController {
public function createDeliveryEntityFormBuilder($entity, $view) {
$formBuilder = parent::createEntityFormBuilder($entity, $view);
$fields = $formBuilder->all();
/**
* #var $fieldId string
* #var $field FormBuilder
*/
foreach ($fields as $fieldId => $field) {
if ($fieldId == 'weeklyMenu') {
$options = [
'attr' => ['size' => 1,],
'required' => true,
'multiple' => false,
'expanded' => false,
'class' => 'App\Entity\WeeklyMenu',
];
$options['query_builder'] = function (EntityRepository $er) {
$qb = $er->createQueryBuilder('e');
return $qb->where($qb->expr()->gt('e.date', ':today'))
->setParameter('today', new \DateTime("today"))
->andWhere($qb->expr()->eq('e.delivery', ':true'))
->setParameter('true', 1)
->orderBy('e.date', 'DESC');
};
$formBuilder->add($fieldId, EntityType::class, $options);
}
}
return $formBuilder;
}
}
so the easyAdmin check if a formbuilder exists with the entity's name i.e. create<ENTITYNAME>FormBuilder(); and you can override here with your own logic.
Another approach to this would be to create new FormTypeConfigurator and overwrite choices and/or labels. And tag it as:
App\Form\Type\Configurator\UserTypeConfigurator:
tags: ['easyadmin.form.type.configurator']
and the configurator looks like this:
<?php
declare(strict_types = 1);
namespace App\Form\Type\Configurator;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\TypeConfiguratorInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormConfigInterface;
final class UserTypeConfigurator implements TypeConfiguratorInterface
{
/**
* {#inheritdoc}
*/
public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig)
{
if ($parentConfig->getData() instanceof User) {
$options['choices'] = User::getUserStatusAvailableChoices();
}
return $options;
}
/**
* {#inheritdoc}
*/
public function supports($type, array $options, array $metadata)
{
return in_array($type, ['choice', ChoiceType::class], true);
}
}
Is there a correct way to customize a form depending on the role of the user that requests it?
My scenario is pretty simple: I need to hide some fields if the user has not the ROLE_ADMIN granted. I tried to avoid the field display on Twig, but
{% if is_granted('ROLE_ADMIN') %}
{{form_row(form.field)}}
{% endif %}
not works, because the form builder bypass this check.
Symfony version: 2.8.2
EDIT
Thanks to the #Rooneyl suggestion I've found the solution:
At first, you need to add the 'role' key to the options parameter. So, in the configureOptions() $options['role'] is always ROLE_USER.
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Ticket',
'role' => 'ROLE_USER'
));
}
Then in the controller you have to pass the getRoles() array:
$user_roles = $this->getUser()->getRoles();
$form = $this->createForm('MyBundle\Form\TicketType', $ticket, array('role' => $user_roles));
You can do it in your form.
Make a service for your form
app.form.type.task:
class: AppBundle\Form\FormType
arguments: ["#security.authorization_checker"]
tags:
- { name: form.type }
In your FormType, add a constructor to get your service.
private $authorization;
public function __construct(AuthorizationChecker $authorizationChecker)
{
$this->authorization = $authorizationChecker;
}
Then, in your builder, you will be able to check user permission
$builder->add('foo');
if($this->authorization->isGranted('ROLE_ADMIN'))
{
$builder->add('bar');
}
And then, finally, you can render your form
{% if form.formbar is defined %}
{{ form_row(form.formbar ) }}
{% endif %}
Please note that it mean that your field may be null. Because maybe you want to see some of them visible by some users and others not.
Else, you can set a default value in your entity construct method, to make sure value won't be null if user don't/can't fill it.
You could use an option passed to the form builder to say what elements are generated.
This way you can change the content and validation that gets done (using validation_groups).
For example, your controller (assuming roles is an array);
you controller;
$form = $this->createForm(new MyType(), $user, ['role' => $this->getUser()->getRoles()]);
And your form:
<?php
namespace AppBundle\Form\Entity;
use AppBundle\Entity\UserRepository;
use Symfony\Component\Form\AbstractType,
Symfony\Component\Form\FormBuilderInterface,
Symfony\Component\OptionsResolver\OptionsResolver;
class MyType extends AbstractType
{
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User',
'validation_groups' => ['create'],
'role' => ['ROLE_USER']
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// dump($options['roles']);
if (in_array('ROLE_ADMIN', $options['role'])) {
// do as you want if admin
$builder
->add('name', 'text');
} else {
$builder
->add('supername', 'text');
}
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_my_form';
}
}
How its possible to join two separated fields (must be separated) in one form (date and time for example) to one entity propery datetime for persisting after form post ?
What is better way ? Data Transofmers ? Form events ? Form Model ? Manual setting all entity properties before persist ?
Entity:
<?php namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="event")
*/
class EventEntity
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
// ...
/**
* #ORM\Column(name="date_time", type="datetime", nullable=false)
*/
protected $datetime;
public function getId()
{
return $this->id;
}
// ...
public function getDateTime()
{
return $this->datetime;
}
public function setDateTime(\DateTime $datetime)
{
$this->datetime = $datetime;
}
}
FormType:
<?php namespace Acme\DemoBundle\Form\Type;
use JMS\DiExtraBundle\Annotation as DI;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('date', 'date', [
'required' => true,
'widget' => 'single_text',
'format' => 'dd.MM.yyyy'
]
)
->add('time', 'time', [
'required' => false,
'widget' => 'single_text'
]
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\EventEntity' //Acme\DemoBundle\Form\Model\EventModel ?
));
}
public function getName()
{
return 'event';
}
}
If you set the date and time widget seperately in the datetime type, then they get seperately rendered, but validated as one field.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('datetime', 'datetime', array(
'date_widget' => 'single_text',
'time_widget' => 'single_text',
'date_format' => 'dd.MM.yyyy',
));
}
I suggest using Pazis solution, since this is the most simple one. But that would also be a perfect job for a DataTransformer:
class MyDataTransformer implements DataTransformerInterface
{
public function transform($value)
{
if (null === $value)
return;
if ($value instanceof \DateTime)
return array(
'date' => $value->format('d.m.Y'),
'time' => $value->format('H:i:s')
);
return null;
}
public function reverseTransform($value)
{
if (null === $value)
return null;
if (is_array($value) && array_key_exists('date', $value) && array_key_exists('time', $value))
return new \DateTime($value['date'] . ' ' . $value['time']);
return null;
}
}
This has the drawback, that you'd need to map every single value in your entity with this transformer, what - for sure - you don't want to. But with small form-tricks, this can be avoided. Therefore you add a subform to your form, which includes a date and a time field and the added Transformer. You'll need to map ("property_path"-option) your DateTime object to this subform or just name it "correctly", so the form framework can map it by name.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('datetime', 'form')
->add('date', 'date', $optionsForDate)
->add('time', 'time', $optionsForTime)
->addViewTransformer(new MyDataTransformer())
);
}
The code may not be perfectly running, but i hope the idea behind splitting one entity property into two (or more) form fields is clear.
héhé, that's a good question.
I would choose the easiest, most generic, reusable solution.
I wouldn't implement methods on my model just for sake of form mapping, but if it makes sense, why not simply using the model api ?
<?php
class EventEntity
{
// assume $this->datetime is initialized and instance of DateTime
public function setDate(\DateTime $date)
{
// i don't know if this works!
$this->datetime->add($this->datetime->diff($date));
}
public function setTime(\DateTime $date)
{
$this->datetime->add($this->datetime->diff($date));
}
}