I'm using Symfony 2 and FOS User Bundle to create a website that handles user information.
I tried to create the edit page to change an existing profile.
My problem is that I want the birthdate to be an input text (because I'm using a Jquery Datapicker). It perfectly works on my register page but when I try to do the same for the profile/edit page, I have a 500 error saying "The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class DateTime. You can avoid this error by setting the "data_class" option to "DateTime" or by adding a view transformer that transforms an instance of class DateTime to scalar, array or an instance of \ArrayAccess.".
Here is how I wrote my User (User.php entity) birthdate parameter:
/**
* #ORM\Column(name="birthdate", type="date")
* #Assert\NotBlank(groups={"Registration", "Profile"})
* #Assert\Date()
*/
protected $birthdate;
In my RegistrationFormType.php (that is working fine):
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder->add('givenname');
$builder->add('familyname');
$builder->add('birthdate', 'text');
...
}
When I try to copy paste this code, I got an error.
The only solution is to do the following:
$builder->add('birthdate', 'date', array('input' => 'datetime'));
This solution does work but is not what I want (it generates three select inputs, I would rather have a text input that uses my Jquery datepicker with my Jquery validation).
Do you have any idea why my error occurs on the edit page and not on the registration page?
Thank you!
I think you need to set the widget option of your date field to text. It's by default set to choice.
From the documentation:
text: renders a three field input of type text (month, day, year).
Example,
$builder->add('birthdate', 'date', array(
'input' => 'datetime',
'widget' => 'text'
));
You can also set it to single_text, which allows you to validate the user's input based on the format option.
Example,
$builder->add('birthdate', 'date', array(
'widget' => 'single_text',
// this is actually the default format for single_text
'format' => 'yyyy-MM-dd',
));
To let it work through a Date Picker, you've then to add the right class to your field.
Related
I'm making an API with Symfony 4 and I'm trying to get it to accept a date, but it keeps throwing the Unable to transform data for property path "dateofbirth": Expected a string. error.
Field in entity:
/**
* #ORM\Column(type="date", nullable=true)
*/
private $dateofbirth;
Field in Form:
->add('dateofbirth', DateType::class, [
'html5' => false,
'required' => false,
'input' => 'string',
'input_format' => 'Y-m-d'
])
Now if I send it JSON with "dateofbirth": "1990-02-12" for example, it throws that error. I've also tried sending something like "dateofbirth": "1990-02-12T00:00:00+01:00" with the 'input_format' set to Y-m-d\TH:i:sP, but I get the same issue.
I've been messing around with the options array for a while now and I've also tried configurations other than the one above, but that's what I have right now. Instead of listing all the ones I've tried that didn't work, I hope someone can just tell me what does.
As per the DateType documentation:
input
The format of the input data - i.e. the format that the date is stored
on your underlying object.
Since you have declared your entity as #Column(type="date") you should set input to datetime. Symfony will create the object according to the input_format and map it to the model.
And since you are sending a single field, set widget to single_text, as the default value of choice will expect an object with day, month, year.
I have made a Data Transformer for my User class on a form field. It is for users to enter another username to which they want to send a private message to. It also has a title and content, but that doesn't matter, for now.
Here is the form builder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('content', 'textarea')
->add(
'receiver',
'text',
array(
// validation message if the data transformer fails
'invalid_message' => 'That is not a valid user',
)
);
$builder->get('receiver')->addModelTransformer(new UserTransformer($this->manager));
}
What I want to do is get the invalid_message error and put it into a translation file. The problem is that I have to write it here:
->add('receiver',
'text',
array('invalid_message' => 'user.invalid')
Which will be taken from my translations.
Is there a way to have this message in my Entity class along with all the other validators, instead of having it in the form type class? I don't want to spread my validation messages everywhere in my files.
To answer your question, if you really want to store all the message translation keys in your entity, you could store those in a constant array in your entity class.
For example, your entity class may look like :
//..
class Foo
{
const MESSAGES = [
'invalidUser' => 'user.invalid'
];
public static function getMessages()
{
return self::MESSAGES;
}
//..
}
and in your data transformer :
->add('receiver',
'text', [
'invalid_message' => Foo::getMessages()['invalidUser']
]
Still I am failing to fully understand the motivation behind this. You will need at some point to identify the message you want to display in your data transformer. So what is the point to not provide directly the translation key associated to this message, and instead retrieve it through the entity ?
The place where your messages should be gathered is only the translation file. The validators in your entity class, as well as your data transformer, are only there to provide the good translation keys.
Goal
Custom Formelement written as an Extension of the (Doctrine) Entity Field. The Element is rendered as a Select2 Element with Tagging. This allows to choose multiple items from a list or add a new value. If a new value is added, a new entity with the value as the property shall be created and associated to the underlying model of the formType.
What works
I can successfully render the select2 Element with the available entities.
Problem
The submitted value is a string containing the (unique) selected properties which currently causes a validation error (invalid value). It doesn't deliever the ID of the selected entities as the original field does, so I wrote a generic DataTransformer that should be able to deal with it. However I am unable to attach the transformer to the field as intended from the context of the TypeExtension. I would have to add the transformer inside my form. As another option I considered is to suppress the validation Listener enitrely, but this would not only disable the validation of the enitre form, it wouldn't be helpful in correctly mapping the selected values to the enitities.
What's the best way to implement this functionality? As the entity FieldType already offers most of the functionality, I would rather try to avoid to write a completly new fieldType.
Update: Why I can't attach the transformer
One option would of course be to do it just the way the cookbook entry suggests, that is to attach the transformer whenever I use the field:
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$entityManager = $options['em'];
$transformer = new ObjectListToStringTransformer($this->em,
[
'class' => $options['class'],
'property' => $options['property'],
'delimiter' => ','
]
);
$builder->add(
$builder->create('entityType', 'entity',
array(
'select2' => true, /*Triggers the select2 template to be used*/
'multiple' => true,
'class' => 'Bundle:Entity',
'property' => 'name'
)
)->addModelTransformer($transformer)
);
}
// ....
}
That could work. But this is not verbose and not typesafe. If I set select2 => true the Transformer must be added otherwise it will not validate. So I'd really like to have that done within my Extension. My first approach to achieve this was simply wrong, just the way the cookbook entry mentioned "how NOT to do it":
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['select2']) {
$builder->addModelTransformer(new ObjectListToStringTransformer($this->em,
[
'class' => $options['class'],
'property' => $options['property'],
'delimiter' => ','
]
)
);
}
parent::buildForm($builder, $options);
}
This will add a transformer to the entire form, not only this field. AFAIK I can't do create(...)->addModelTransformer($transformer) as I probably could if it was a sub class (vertical inheritance) but not inside the extension (horizontal inheritance). Is there a way to add it after the field was already added? Or is there a way to achieve this by creating a subclass of the choice field?
Apparently the entity field itself has a registered ModelTransformer (or a ViewTransformer) which get called before my ModelTransformer. Since this always fails, my Transformer is never reached. To get around this, I changed addModelTransformer to addViewTransformer. While the code of my transformer wasn't working yet, the interaction with it does.
I have a OneToMany association between a Server entity and Client entities in the database. One server can have many clients. I want to make a form where the user can choose a server from a dropdown, fill in some details for a new client, and submit it.
Goal
To create a form where a user can input data into fields for the Client, choose a Server from the dropdown, then click submit and have this data (and the association) persisted via Doctrine.
Simple, right? Hell no. We'll get to that. Here's the pretty form as it stands:
Things of note:
Server is populated from the Server entities (EntityRepository::findAll())
Client is a dropdown with hardcoded values
Port, endpoint, username and password are all text fields
Client Entity
In my infinite wisdom I have declared that my Client entity has the following constructor signature:
class Client
{
/** -- SNIP -- **/
public function __construct($type, $port, $endPoint, $authPassword, $authUsername);
/** -- SNIP -- **/
}
This will not change. To create a valid Client object, the above constructor parameters exist. They are not optional, and this object cannot be created without the above parameters being given upon object instantiation.
Potential Problems:
The type property is immutable. Once you've created a client, you cannot change the type.
I do not have a setter for type. It is a constructor parameter only. This is because once a client is created, you cannot change the type. Therefore I am enforcing this at the entity level. As a result, there is no setType() or changeType() method.
I do not have the standard setObject naming convention. I state that to change the port, for example, the method name is changePort() not setPort(). This is how I require my object API to function, before the use of an ORM.
Server Entity
I'm using __toString() to concatenate the name and ipAddress members to display in the form dropdown:
class Server
{
/** -- SNIP -- **/
public function __toString()
{
return sprintf('%s - %s', $this->name, $this->ipAddress);
}
/** -- SNIP -- **/
}
Custom Form Type
I used Building Forms with Entities as a baseline for my code.
Here is the ClientType I created to build the form for me:
class ClientType extends AbstractType
{
/**
* #var UrlGenerator
*/
protected $urlGenerator;
/**
* #constructor
*
* #param UrlGenerator $urlGenerator
*/
public function __construct(UrlGenerator $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** Dropdown box containing the server name **/
$builder->add('server', 'entity', [
'class' => 'App\Model\Entity\Server',
'query_builder' => function(ServerRepository $serverRepository) {
return $serverRepository->createQueryBuilder('s');
},
'empty_data' => '--- NO SERVERS ---'
]);
/** Dropdown box containing the client names **/
$builder->add('client', 'choice', [
'choices' => [
'transmission' => 'transmission',
'deluge' => 'deluge'
],
'mapped' => false
]);
/** The rest of the form elements **/
$builder->add('port')
->add('authUsername')
->add('authPassword')
->add('endPoint')
->add('addClient', 'submit');
$builder->setAction($this->urlGenerator->generate('admin_servers_add_client'))->setMethod('POST');
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\Model\Entity\Client',
'empty_data' => function(FormInterface $form) {
return new Client(
$form->getData()['client'],
$form->getData()['port'],
$form->getData()['endPoint'],
$form->getData()['authPassword'],
$form->getData()['authUsername']
);
}
]);
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'client';
}
}
The above code is what actually generates the form to be used client-side (via twig).
The Problems
First and foremost, with the above code, submitting the form gives me:
NoSuchPropertyException in PropertyAccessor.php line 456:
Neither the property "port" nor one of the methods "addPort()"/"removePort()", "setPort()", "port()", "__set()" or "__call()" exist and have public access in class "App\Model\Entity\Client".
So it can't find the port method. That's because it's changePort() as I explained earlier. How do I tell it that it should use changePort() instead? According to the docs I would have to use the entity type for port, endPoint etc. But they're just text fields. How do I go about this the right way?
I have tried:
Setting ['mapped' => false] on port, authUsername etc. This gives me null for all the client fields, but it does seem to have the relevant server details with it. Regardless, $form->isValid() return false. Here's what var_dump() shows me:
A combination of other things involving setting each on field to "entity", and more..
Basically, "it's not working". But this is as far as I've got. What am I doing wrong? I am reading the manual over and over but everything is so far apart that I don't know if I should be using a DataTransformer, the Entity Field Type, or otherwise. I'm close to scrapping Symfony/Forms altogether and just writing this myself in a tenth of the time.
Could someone please give me a solid answer on how to get where I want to be? Also this may help future users :-)
There are a few problems with the above solution, so here's how I got it working!
Nulls
It turns out that in setDefaultOptions(), the code: $form->getData['key'] was returning null, hence all the nulls in the screenshot. This needed to be changed to $form->get('key')->getData()
return new Client(
$form->get('client')->getData(),
$form->get('port')->getData(),
$form->get('endPoint')->getData(),
$form->get('authPassword')->getData(),
$form->get('authUsername')->getData()
);
As a result, the data came through as expected, with all the values intact (apart from the id).
Twig Csrf
According to the documentation you can set csrf_protection => false in your form options. If you don't do this, you will need to render the hidden csrf field in your form:
{{ form_rest(form) }}
This renders the rest of the form fields for you, including the hidden _token one:
Symfony2 has a mechanism that helps to prevent cross-site scripting: they generate a CSRF token that have to be used for form validation. Here, in your example, you're not displaying (so not submitting) it with form_rest(form). Basically form_rest(form) will "render" every field that you didn't render before but that is contained into the form object that you've passed to your view. CSRF token is one of those values.
Silex
Here's the error I was getting after solving the above issue:
The CSRF token is invalid. Please try to resubmit the form.
I'm using Silex, and when registering the FormServiceProvider, I had the following:
$app->register(new FormServiceProvider, [
'form.secret' => uniqid(rand(), true)
]);
This Post shows how Silex is giving you some deprecated CsrfProvider code:
Turned out it was not due to my ajax, but because Silex gives you a deprecated DefaultCsrfProvider which uses the session ID itself as part of the token, and I change the ID randomly for security. Instead, explicitly telling it to use the new CsrfTokenManager fixes it, since that one generates a token and stores it in the session, such that the session ID can change without affecting the validity of the token.
As a result, I had to remove the form.secret option and also add the following to my application bootstrap, before registering the form provider:
/** Use a CSRF provider that does not depend on the session ID being constant. We change the session ID randomly */
$app['form.csrf_provider'] = $app->share(function ($app) {
$storage = new Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage($app['session']);
return new Symfony\Component\Security\Csrf\CsrfTokenManager(null, $storage);
});
With the above modifications, the form now posts and the data is persisted in the database correctly, including the doctrine association!
How would I go about setting the default value of the datetime field of a form to the current time in Symfony2?
I'm using the FormBuilderInterface and the following isn't working:
$builder->add(
'completed_datetime', 'datetime', array('data' => new \DateTime('now'))
);
The form continues to show what I presume to be the default value for the datetime field 2008-01-01 00:00.
In the end it turned out I was viewing a cached version of my page. Ctrl+F5 cleared the cache and showed the desired result.
I was able to achieve the functionality I wanted in both the ways posted here, i.e.:
In the Type class:
$builder->add(
'completed_datetime', 'datetime', array('data' => new \DateTime('now'))
);
And in my controller:
$task->setCompletedDateTime(new \DateTime('now'));
I believe it's also possible to set it in my Task Entity class' constructor.