Symfony 4 forms, custom DTO and entity relationships - symfony

An API has been created to allow Posts to be created with a description and any number of attached Photos.
The problem was that when an API request to edit a Post was received with a null description the typehint would fail.
class Post {
/**
* #Assert\NotBlank
* #ORM\Column(type="text")
*/
private $description;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Photo")
*/
private $photos;
public function setDescription(string $descripton)
Which means instead of the Symfony validation failing Assert\NotBlank it was returning a 500.
This could have been fixed by allowing nulls in the method ?string, this would allow the validation to be called, but result in a dirty entity.
The DTO (Data Transfer Object) approach, a new class to represent the data was created and the validation rules applied to this, this was then added to the form.
class PostData {
/**
* #Assert\NotBlank
*/
public $description;
/**
* #Assert\Valid
* #var Photo[]
*/
public $photos;
The form has was modified:
class PostType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
$builder
->add('description')
->add('photos', EntityType::class, [
'class' => Photo::class,
'multiple' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => PostData::class,
));
}
This worked for description it could be validated without changing the Post entity. If a null was received PostData would trigger the Assert\NotBlank and Post::setDescription would not be called with null.
The problem came when trying to validate Photos existed, if the photo existed it would work, if it didn't there would be a 500 error.
Potentially meaningless 500 error which doesn't indicate the reason
Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is not supported. (500 Internal Server Error)
How can I use DTO PostData to validate a Photo entity exists?

Update composer.json and run composer update
"symfony/http-foundation": "4.4.*",
The issue is related to https://github.com/symfony/symfony/issues/27339
This will give a more meaningful Symfony Form error
Unable to reverse value for property path \"photos\": Could not find all matching choices for the given values
It will also return a lot of extra information if you serilize form errors including DATABASE_URL and APP_SECRET.
I do not recommend running this in production.

Related

Symfony 3 Constraint validation Date or Datetime

I try to validate a date (or a datetime) with the validation of a form into Symfony (3.2).
I'm using FOSRestBundle to use the json from request (because i try to develop my personnal API)
But i've try a lot of format:
2017-04-09
17-04-09
for datetime:
2017-04-09 21:12:12
2017-04-09T21:12:12
2017-04-09T21:12:12+01:00
...
But the form is not valid and i get always this error:
This value is not valid
The function of my controller
public function postPlacesAction(Request $request) {
$place = new Place();
$form = $this->createForm(PlaceType::class, $place);
$form->handleRequest($request);
if ($form->isValid()) {
return $this->handleView($this->view(null, Response::HTTP_CREATED));
} else {
return $this->handleView($this->view($form->getErrors(), Response::HTTP_BAD_REQUEST));
}
}
My entity
class Place
{
/**
* #var string
*
* #Assert\NotBlank(message = "The name should not be blank.")
*/
protected $name;
/**
* #var string
*
* #Assert\NotBlank(message = "The address should not be blank.")
*/
protected $address;
/**
* #var date
*
* #Assert\Date()
*/
protected $created;
// ....
// Getter and setter of all var
My entity type
class PlaceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('address');
$builder->add('created');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'MyBundle\Entity\Place',
'csrf_protection' => false
]);
}
}
An example of request (i'm using Postman)
Method: POST
Header: application/json
Body (raw):
{"place":{"name":"name","address":"an address","created":"1997-12-12"}}
I'm not sure that i use the right format, or if i missing anything in my files :/
Could you please switch on the light in my mind!?! :)
Thanks so much for your help.
Fabrice
The problem at created field in your form type. When you add created field using $builder->add('created'); syntax, the default type Symfony\Component\Form\Extension\Core\Type\TextType will be applied and 1997-12-12 input data is a string, not a DateTime instance.
To fix this issue, you should pass DateType in second argument: $builder->add('created', 'Symfony\Component\Form\Extension\Core\Type\DateType');. This form type has a transformer which will transform the input data 1997-12-12 into a DateTime instance.
For more informations about Symfony's form types, have a look at Form Types Reference

Sf2/3 Displaing different forms for Different roles(users)

I have an App that requires a complex access control. And the Voters is what I need to make decisions on Controller-level.
However, I need to build form for different users by different way.
Example: There are Admin(ROLE_ADMIN) and User(ROLE_USER). There is a Post that contains fields:
published
moderated
author
body
timestamps
Admin must be able to edit all fields of any Post.
User - only particular fields: published, body. (bay the way, only if this is an author of this post, but this is decided by voters).
Possible solution i found is dynamic form modification. But if we need more complexity, for example posts belongs to Blog, Blog belongs to author. And Post can be edited by direct author and author of the blog.
And Author of the Blog can also edit postedAt field, but it can't be done by direct author of the post.
I need to write some login in PRE_BIND listener.
Maybe there is some kind of common practice for that situation, or someone can show their own examples of.
You can do this creating a form type extension
Imagine a form type where you want to display a field only if ROLE_ADMIN is granted. For that you can simply add a new property to the field ('author' in this example)
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('published', 'text')
->add('moderated', 'text')
->add('author', 'text', [
'is_granted' => 'ROLE_ADMIN',
])
;
}
For this parameter to be interpreted, you must create a form type extension by injecting the SecurityContext Symfony to ensure the rights of the logged on user.
<?php
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\SecurityContextInterface;
class SecurityTypeExtension extends AbstractTypeExtension
{
/**
* The security context
* #var SecurityContextInterface
*/
private $securityContext;
/**
* Object constructor
*/
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$grant = $options['is_granted'];
if (null === $grant || $this->securityContext->isGranted($grant)) {
return;
}
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
if ($form->isRoot()) {
return;
}
$form->getParent()->remove($form->getName());
});
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefined(array('is_granted' => null));
}
/**
* {#inheritdoc}
*/
public function getExtendedType()
{
return 'form';
}
}
Finally, you just have to save the extension as a service :
services:
yourbundle.security_type_extension:
class: YourProject\Bundle\ForumBundle\Form\Extension\SecurityTypeExtension
arguments:
- #security.context
tags:
- { name: form.type_extension, alias: form }
Dynamic form modification seems unnecessary. Once the user is logged in the roles should not change.
You could inject the security.authorization_checker service in your form type and use that in the buildForm method to conditionally add fields to your form. Depending on how much the forms differ, this might become messy with too many if-statements. In that case I would suggest writing different form types altogether (possibly extending a base form type for repeated things).

Symfony custom form weird property access errors

I've got this strange problem, here is example usage of my custom ThingType class.
->add('photos', 'namespace\Form\Type\ThingType', [
'required' => false,
])
if the field name is photos everything works as expected, but if I change my entity field to let's say photosi, run generate entities, and change the form field name, this error is thrown:
Neither the property "photosi" nor one of the methods
"addPhotosus()"/"removePhotosus()", "setPhotosi()", "photosi()",
"__set()" or "__call()" exist and have public access in class
"AppBundle\Entity\Product".
I guess the problem comes from Symfony trying to generate getter method name for my entity. Why is this addPhotosus method name generated? How can I solve this?
EDIT:
I'm using model transformer when showing the data to the user.
$builder->addModelTransformer(new CallbackTransformer(
function ($imagesAsText) {
if (!$imagesAsText) {
return null;
}
$newImages = [];
foreach($imagesAsText as $img) {
$newImages[] = $img->getID();
}
return implode(',', $newImages);
},
function ($textAsImages) use ($repo) {
$images = [];
foreach(explode(',', $textAsImages) as $imgID) {
$img = $repo->findOneById($imgID);
if ($img) {
$images[] = $img;
}
}
return $images;
}
));
The actual field is TextType::class with entity ids in it for example 1,10,32,51. The model transformer transforms this data to entities. Setting 'data_class' to my form type seems irrelevant, because the actual form type is a part of entity. I mean I have Product entity and Photo entity, photos is array of photo entity. So in my ThingType, what data_class should I use, photo or product?
Thanks
The fist parameter of the add method for a form, should be one of the mapped attributes of the data_class of the form, usually selected inside the form as
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
That isn't related to the form name. So , you are trying to access to a "photosi" attribute inside your Product class.
Hope this help you.
Ok so for the first point you need to remember that Symfony is looking for setXX() and getXX()method in your entity for each entry of your form.
If you change your variable name you need to update the form :
->add('newName', XXType::class, [
'required' => false,
])
and you're entity by changing the variable
class Entity
{
/**
* #ORM\Column(type="string", length=255)
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName(oldName){
$this->oldName = $oldName;
return $this
}
}
then run the command
php bin/console make:entity --regenerate
and symfony will upload your entity by itself
class Entity
{
/**
* #ORM\Column(type="string", length=255)
* #SerializedName("title")
* #Groups({"calendar"})
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName($oldName){
$this->oldName = $oldName;
return $this
}
public function getNewName(){
return $this->newName;
}
public function setNewName($newName){
$this->newName = $newName;
return $this
}
note that the old get and set method are not deleted by the script
note as well that in your specific case of photosi, symfonyguess that the "i" is a plural mark and look for addPhotosus() methods
For the edit it looks very unclear and has nothing to do with the first question. Consider reading : doc on collectionType

Symfony2/Doctrine UniqueEntity on ManyToOne entity ignored

I have "roles" associated to "projects".
I don't care if a role name is duplicated, but what I want to make sure is that for each project the role name cannot be duplicated.
Here is what I thought should work:
<?php
// src/AppBundle/Entity/Role.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\RoleRepository")
* #ORM\Table(name="roles")
* #UniqueEntity(fields={"name","project"}, message="Duplicated role for this project")
*/
class Role
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $name;
/**
* #ORM\Column(type="text")
*/
protected $description;
...
other fields
...
/**
* #ORM\ManyToOne(targetEntity="Project")
*/
protected $project;
}
According to the documentation here that's exactly what I need:
This required option is the field (or list of fields) on which this
entity should be unique. For example, if you specified both the email
and name field in a single UniqueEntity constraint, then it would
enforce that the combination value where unique (e.g. two users could
have the same email, as long as they don't have the same name also).
The constraint is simply ignored (I mean that if I try to have the same role name for the same project, it stores the duplicated role name and projectID).
What am I missing?
EDIT: after I updated the DB with "php app/console doctrine:schema:update --force" I tried to generate the error directly with SQL, but no exceptions were thrown. Now, I don't know if this "UniqueEntity" validation is done at DB level or it's Doctrine's validator.
EDIT2: I tried to have only one field ("name") and the validation works properly (only on that field of course). I also tried to have validation on the fields "name" and "description" and it works!!! So basically it does not validate if the field to be validated is the ID pointing to another table.
Anyways, here is the controller:
/**
* #Route("/role/create/{projectID}", name="role_create")
*/
public function createRoleAction(Request $request, $projectID)
{
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($projectID);
$role = new Role();
$form = $this->createForm(new RoleFormType(), $role);
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$role->setProject($prj);
$em->persist($role);
$em->flush();
return $this->redirect($this->generateUrl('hr_manage', array('projectID' => $projectID)));
}
return $this->render('Role/createForm.html.twig', array('projectID' => $projectID, 'form' => $form->createView(),));
}
The validation is not performed, and the entity persisted on DB, with the "project" column pointing at the right project. Here is a snapshot of the 2 relevant fields:
Here is the RoleFormType (an extract of relevant fields):
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RoleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// add your custom field
$builder->add('name', 'text')
->add('description', 'text')
...lots of other fields, but "project" is not present as it's passed automatically from the controller
->add('save', 'submit', array('label' => 'Create'));
}
public function getName()
{
return 'role';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Role',));
}
}
The problem is you are not actually validating the entity to check for the unique constraint violation. When you call $form->isValid(), it does call the validator for the Role entity since you passed that as the form's data class. However, since project is not set until after that, no validation occurs on the project field.
When you call $em->persist($role); and $em->flush(); this simply tells Doctrine to insert the entity into the database. Those 2 calls do not perform validation on their own, so the duplicates will be inserted.
Try setting the project before creating the form instead:
$role = new Role();
$role->setProject($prj);
$form = $this->createForm(new RoleFormType(), $role);
Now project will be set on the entity, so when $form->isValid() is called the Symfony validator will check for uniqueness.
If that doesn't work, you'll want to add a project type to the form as a hidden field as well so it's passed back, but I don't think that will be necessary.
The other thing I would state is that you definitely want to add the unique constraint on your database itself - this way even if you try to insert a duplicate, the database will thrown an exception back to you and not allow it, regardless of your code.

entities in different bundles

I'm using Symfony 2 and I have two entities in different bundles like:
//this class overrides fos_user class
//User\UserBundle\Entity\User
class User extends BaseUser
{
//..
/**
* #ORM\OneToMany(targetEntity="News\AdminBundle\Entity\News", mappedBy="author_id")
*/
protected $news_author;
//...
}
//News\AdminBundle\Entity\News
class News
{
//...
/**
* #ORM\ManyToOne(targetEntity="\User\UserBundle\Entity\User", inversedBy="news_author")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
protected $news_author;
//...
}
Both classes (entities) works fine. I have successfully setup fos_user bundle with registration and other stuff. The same if for News class. Then I build relation between those two classes OneTo Many (User -> News) as it is shown in code. This also works fine without errors and I can add news that belongs to user. The problem is when I build a form with entity class like:
->add('year', 'entity', array(
'class' => 'NewsAdminBundle:News',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->groupBy('u.year')
->orderBy('u.year', 'DESC');
},))
This form shows me a years when news are posted (like archive). Years are showing fine, but when I submit (post) a form then I've got error:
Class User\UserBundle\Entity\News does not exist
I figure out that this error is connected with sentence
$form->bindRequest($request);
The problem is because I have two entities in different bundles. How can I solve this error?
Edit:
I solved the problem. When I run
php app/console doctrine:generate:entities User
php app/console doctrine:generate:entities News
then Doctrine generate getters and setters in User and News. In entity News it generates method
/**
* Add news_author
*
* #param User\UserBundle\Entity\News $newsAuthor
*/
public function addNews(User\UserBundle\Entity\News $newsAuthor)
{
$this->news_author[] = $newsAuthor;
}
I was not paying attention to this method and I change it to this
/**
* Add news_author
*
* #param News\AdminBundle\Entity\News $newsAuthor
*/
public function addNews(News\AdminBundle\Entity\News $newsAuthor)
{
$this->news_author[] = $newsAuthor;
}
Now everything works fine. Thanks for all answers.
/**
* #ORM\ManyToOne(targetEntity="User\UserBundle\Entity\User", inversedBy="news_author")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
protected $news_author;
You have to remove prefix backslash – see note in Doctrine documentation

Resources