I am trying to submit a JSON through a complex form. I can't figure out what I am missing. The "normal" form is functioning. I am able to get serialized data with groups.
Class TaskBoard
class TaskBoard
{
/**
* #var integer $id id
*
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var \DateTime $createdTime createdTime
*
* #ORM\Column(type="datetime")
* #Assert\DateTime()
*/
private $createdTime;
/**
* #var \DateTime $lastUpdatedTime lastUpdatedTime
*
* #ORM\Column(type="datetime", nullable=true)
* #Assert\DateTime()
*/
private $lastUpdatedTime;
/**
* #var string $name name
*
* #ORM\Column(type="string", length=20)
* #Assert\Type("string")
* #Assert\NotBlank()
* #Assert\Length(
* min = 2,
* max = 20,
* minMessage = "The name must be at least {{ limit }} characters long",
* maxMessage = "The name cannot be longer than {{ limit }} characters"
* )
*/
private $name;
/**
* #var App\Entity\User $user user
*
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="taskboards", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* #var string $description description
*
* #ORM\Column(type="text", nullable=true)
* #Assert\Type("string")
*/
private $description;
/**
* #var App\Entity\Status $status status
*
* #ORM\ManyToOne(targetEntity="App\Entity\Status", inversedBy="taskboards", cascade={"persist"})
* #ORM\JoinColumn(name="status_id", referencedColumnName="id", nullable=false)
*/
private $status;
/**
* #var boolean $completed completed
*
* #ORM\Column(type="boolean")
*/
private $completed;
/**
* #var \DateTime $deadLine deadLine
*
* #ORM\Column(type="date", nullable=true)
* #Assert\DateTime()
* #Assert\GreaterThanOrEqual("today")
*/
private $deadLine;
Class Status
class Status
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=20)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\TaskBoard", mappedBy="status", cascade={"persist"})
*/
public $taskboards;
Class User
class User extends BaseUser
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\TaskBoard", mappedBy="user", cascade={"persist"})
*/
public $taskboards;
Form
class TaskBoardType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name', TextType::class)
->add('user', EntityType::class, array(
'class' => User::class,
'choice_label' => 'username',
)
)
->add('description', TextareaType::class, array(
'required' => false
))
->add('status', EntityType::class, array(
'class' => Status::class,
'choice_label' => 'name',
)
)
->add('deadLine', DateTimeType::class)
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => TaskBoard::class,
'csrf_protection' => false,
]);
}
Controller
class TaskBoardAPIController extends AbstractController {
public function postTaskBoard(Request $request) {
$form = $this->createForm(TaskBoardType::class, new TaskBoard());
$data = json_decode(
$request->getContent(), true
);
var_dump($data);
$form->submit($data);
if (!$form->isValid()) {
return new JsonResponse(
[
'status' => 'error',
'errors' => $form->getErrors(),
'form' => $form,
], JsonResponse::HTTP_BAD_REQUEST
);
}
$this->entityManager->persist($form->getData());
$this->entityManager->flush();
return new JsonResponse(
[
'status' => 'ok',
], JsonResponse::HTTP_CREATED
);
}
JSON sent
{
"name": "XXX",
"user": {
"id": 1,
"username": "BFA"
},
"description": "XXXXXXXXXXXXXXX",
"status": {
"id": 1,
"name": "To Do"
},
"completed": false
}
The form is not valid and blank in the JsonResponse.
I based myself on : https://codereviewvideos.com/course/beginners-guide-back-end-json-api-front-end-2018/video/symfony-4-json-api-form-submission
and Deserialize an entity with a relationship with Symfony Serializer Component
Thanks for your help.
What was wrong was the JSON input.
The form does this :
class TaskBoardType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name', TextType::class)
->add('user', EntityType::class, array(
'class' => User::class,
'choice_label' => 'username',
)
)
->add('description', TextareaType::class, array(
'required' => false
))
->add('status', EntityType::class, array(
'class' => Status::class,
'choice_label' => 'name',
)
)
->add('deadLine', DateTimeType::class)
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => TaskBoard::class,
'csrf_protection' => false,
]);
}
When checking the code generated from the form, this is the result :
<div>
<label for="task_board_user" class="required">User</label>
<select id="task_board_user" name="task_board[user]">
<option value="1">XXX</option>
<option value="2">XXX</option>
</select>
</div>
Thus the form is expecting directly an INT/ID.
By changing the JSON as follow it goes through validation :
{
"name": "XXXO",
"user": 1,
"description": "XXXXXXXXXXXXXXX",
"status": 1
}
Your forgot to handle and get form submission.
Note that you will need to get request data using something like
$form = $this->createFormBuilder($task)
... add fields
->getForm();
$form->handleRequest($request); // handling request
if ($form->isSubmitted() && $form->isValid()) {
// $form->getData() holds the submitted values
// but, the original `$task` variable has also been updated
$task = $form->getData();
// ... perform some action, such as saving the task to the database
// for example, if Task is a Doctrine entity, save it!
// $entityManager = $this->getDoctrine()->getManager();
// $entityManager->persist($task);
// $entityManager->flush();
return $this->redirectToRoute('...');
}
for more, see Handling Form Submissions
Related
I'm using Symfony 5.3.7, and using the #Assert annotations, attempting to validate a new user upon registering. Trying to understand the docs however not getting anywhere. Ideally I'd like to be able to understand how to validate an entity using these annotations. I've tried using a separate class, and got nowhere with understanding, although I do want to go down this route in case i reuse the component elsewhere. (I'd rather not validate in the form, but if i have to I can)
Entity
class User implements UserInterface
{
use TimestampableEntity;
/**
* #var int
*
* #ORM\Id()
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(name="intUserId", type="integer", nullable=false)
*/
private int $id;
/**
* #var string
*
* #ORM\Column(name="strFirstName", type="string", nullable=false)
*
* #Assert\NotBlank
* #Assert\Length(
* min = 2,
* max = 50,
* minMessage = "Your first name must be at least {{ limit}} characters long",
* maxMessage = "Your first name cannot be longer than {{ limit }} characters"
* )
*/
private string $firstName;
/**
* #var string
*
* #ORM\Column(name="strLastName", type="string", nullable=false)
*
* #Assert\NotBlank
* #Assert\Length(
* min = 2,
* max = 50,
* minMessage = "Your first name must be at least {{ limit}} characters long",
* maxMessage = "Your first name cannot be longer than {{ limit }} characters"
* )
*/
private string $lastName;
/**
* #var string
*
* #ORM\Column(name="strUsername", type="string", nullable=false)
*
* #Assert\Unique()
* #Assert\Length(
* min = 2,
* max = 15,
* minMessage = "Your first name must be at least {{ limit}} characters long",
* maxMessage = "Your first name cannot be longer than {{ limit }} characters"
* )
*/
private string $username;
/**
* #var string
*
* #ORM\Column(name="strPassword", type="string", nullable=false)
*
* #Assert\NotNull()
* #Assert\Regex(pattern = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!#$%^&*-]).{8,}$")
*
* #SecurityAssert\UserPassword(message = "Password is incorrect, please try again")
*/
private string $password;
/**
* #var string
*
* #ORM\Column(name="strEmail", type="string", nullable=false)
*
* #Assert\Unique()
* #Assert\Regex(pattern = "\b[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}\b")
*/
private string $email;
/**
* #var boolean
*
* #ORM\Column(name="bolAcceptTermsConditions", type="bool", nullable=false)
*
* #Assert\NotNull()
*/
private bool $acceptTermsAndConditions;
/**
* #var boolean
*
* #ORM\Column(name="bolAcceptPrivacyPolicy", type="bool", nullable=false)
*
* #Assert\NotNull()
*/
private bool $acceptPrivacyPolicy;
/**
* #var boolean
*
* #ORM\Column(name="bolEmailOptIn", type="bool", nullable=false)
*
* #Assert\NotNull()
*/
private bool $emailOptIn;
/**
* User constructor.
*
* #param string $firstName
* #param string $lastName
* #param string $username
* #param string $email
* #param string $password
*/
public function __construct(
string $firstName,
string $lastName,
string $username,
string $email,
string $password,
bool $acceptTermsAndConditions = false,
bool $acceptPrivacyPolicy = false,
bool $emailOptIn = false
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->username = $username;
$this->email = $email;
$this->password = $password;
$this->acceptTermsAndConditions = $acceptTermsAndConditions;
$this->acceptPrivacyPolicy = $acceptPrivacyPolicy;
$this->emailOptIn = $emailOptIn;
$this->dtmAdded = Carbon::now();
}
Ideally I'd pass in the new entity or the array, either create the entity in the validator or similar
Controller
public function register(Request $request): Response
{
$registrationForm = $this->createForm(RegistrationFormType::class);
$registrationForm->handleRequest($request);
if ($registrationForm->isSubmitted() && $registrationForm->isValid()) {
$newUserData = $registrationForm->getData();
$user = new User(
$newUserData['firstName'],
$newUserData['lastName'],
$newUserData['email'],
$newUserData['username'],
$newUserData['password'],
$newUserData['termsConditions'],
$newUserData['privacyPolicy'],
$newUserData['emailFrequency']
);
return new RedirectResponse($request->headers->get('referer'));
}
return new RedirectResponse($request->headers->get('referer'));
}
Form
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RegistrationFormType extends AbstractType
{
public const FORM_NAME = 'registrationForm';
/**
* Form Builder for Registration Form
*
* #param FormBuilderInterface $builder
* #param array $options
*
* #return void
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('firstName', TextType::class, [
'label' => 'Firstname:',
'required' => true,
'attr' => [
'class' => 'form-input input-type-text',
'placeholder' => 'John',
],
]);
$builder->add('lastName', TextType::class, [
'label' => 'Lastname:',
'required' => true,
'attr' => [
'class' => 'form-input input-type-text',
'placeholder' => 'Doe'
],
]);
$builder->add('email', EmailType::class, [
'label' => 'Email:',
'required' => true,
'attr' => [
'class' => 'form-input input-type-email',
'placeholder' => 'example#example.com'
]
]);
$builder->add('username', TextType::class, [
'label' => 'Username:',
'required' => true,
'attr' => [
'class' => 'form-input input-type-text',
]
]);
$builder->add('password', PasswordType::class, [
'label' => 'Password:',
'required' => true,
'attr' => [
'class' => 'form-input input-type-email',
]
]);
$builder->add('termsConditions', CheckboxType::class, [
'label' => 'I accept the terms & conditions',
'required' => true,
]);
$builder->add('privacyPolicy', CheckboxType::class, [
'label' => 'I have read and understood the privacy policy',
'required' => true,
]);
$builder->add('emailFrequency', ChoiceType::class, [
'label' => 'Opt in to emails:',
'multiple' => false,
'expanded' => true,
'choices' => [
'Yes' => true,
'No' => false,
],
]);
}
/**
* #param OptionsResolver $resolver
*
* #return void
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
/**
* #return string
*/
public function getBlockPrefix(): string
{
return self::FORM_NAME;
}
}
so the issue is that you are not passing the new User object to the form, so the validation will not happen in this case.
What you can do to fix this is, declare the data_class for the form to be User in this case
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
And when you create the form in the action you need to pass a new User object to it like:
$user = new User();
$registrationForm = $this->createForm(RegistrationFormType::class, $user);
And you can remove the part where you instantiate and send data to the user entity
$user = new User(
$newUserData['firstName'],
$newUserData['lastName'],
$newUserData['email'],
$newUserData['username'],
$newUserData['password'],
$newUserData['termsConditions'],
$newUserData['privacyPolicy'],
$newUserData['emailFrequency']
);
instead of that, you should just use the entity manager to save the new user
, for that, you need to inject the entity manager, like so:
public function register(Request $request, EntityManagerInterface $entityManager): Response
and just use it to save the user object into the database
if ($registrationForm->isSubmitted() && $registrationForm->isValid()) {
$entityManager->persist($user);
$entityManager->flush();
}
I'm trying to upload multiple files on Symfony but when the form is submitted the form image field returns a null object like this
object(Doctrine\Common\Collections\ArrayCollection)#1455 (1) {
["elements":"Doctrine\Common\Collections\ArrayCollection":private]=>
array(1) {
[0]=>
object(AdminBundle\Entity\ImageNew)#1717 (5) {
["nom":"AdminBundle\Entity\ImageNew":private]=>
NULL
["path":"AdminBundle\Entity\ImageNew":private]=>
NULL
["idimage":"AdminBundle\Entity\ImageNew":private]=>
NULL
["categorie":"AdminBundle\Entity\ImageNew":private]=>
NULL
["file":"AdminBundle\Entity\ImageNew":private]=>
NULL
}
}
}
But when I get files directly inside the request files attributes file exist. I've tried to upload a file by accessing the attribute in the request, it works but it still wants to upload file via Symfony $form request handler.
That's my controller
public function addColorAction(Request $request, Article $article)
{
$couleur = new Couleur();
$form = $this->createForm('AdminBundle\Form\CouleurType', $couleur);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$files = $couleur->getImages();
echo "<pre>";
var_dump($files); die;
$imgs = $request->files->get("adminbundle_couleur")["images"];
foreach ($imgs as $img) {
$image = new ImageNew();
$image->setFile($img["file"]);
$image->upload();
$couleur->addImage($image);
$em->persist($image);
$em->flush();
}
$color_art_dispo = new CouleurArticleDispo();
$color_art_dispo->setEnStock(true);
$color_art_dispo->setArticle($article);
$color_art_dispo->setCouleur($couleur);
$em->persist($couleur);
$em->persist($color_art_dispo);
$em->flush();
return $this->redirectToRoute('article_index');
}
return $this->render(
'admin/article/couleur/new.html.twig', array(
'couleur' => $couleur,
'form' => $form->createView(),)
);
}
The couleur entity
class Couleur
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="nom", type="string", length=255)
* #Assert\NotBlank(message="Veuillez entrer le nom de la couleur")
*/
private $nom;
/**
* #var string
*
* #ORM\Column(name="code_couleur", type="string", length=6)
* #Assert\NotBlank(message="Veuillez entrer le code couleur correspondant")
* #Assert\Length(
* min=6,
* max=6,
* minMessage="Le code couleur n'est pas correct.",
* maxMessage="Le code couleur n'est pas correct.",
* )
*/
private $codeCouleur;
/**
*
* #ORM\OneToMany(targetEntity="CouleurArticleDispo", mappedBy="_couleurs")
*/
private $colorArticles;
/**
* Many Colors have Many Images.
*
* #ORM\ManyToMany(targetEntity="ImageNew",cascade={"remove"})
* #ORM\JoinTable(name="color_images",joinColumns={#ORM\JoinColumn(name="color_id",referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="image_id", referencedColumnName="idimage", onDelete="CASCADE")}
* )
*/
private $images;
public function __toString()
{
return (string) $this->getNom();
}
/**
* Class Constructor
*/
public function __construct()
{
$this->images = new ArrayCollection();
}
}
This is the image entity
class ImageNew
{
/**
* #var string
*
* #ORM\Column(name="nom", type="string", length=100)
*/
private $nom;
/**
* #var string
*
* #ORM\Column(name="path", type="string", length=255)
*/
private $path;
/**
* #var integer
*
* #ORM\Column(name="idimage",type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $idimage;
/**
* #var \AdminBundle\Entity\Categorie
*
* #ORM\ManyToOne(targetEntity="AdminBundle\Entity\Categorie",cascade={"persist"},inversedBy="slides")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_categorie",referencedColumnName="idcategorie",nullable=true,onDelete="SET NULL")
* })
*/
private $categorie;
/**
* #Assert\NotNull()
* #Assert\File(
* maxSize = "6000k",
* mimeTypes = {"image/png", "image/jpg", "image/bmp"},
* mimeTypesMessage = "Please upload a valid Image File (PNG, JPEG or BMP)"
* )
*/
private $file;
public function __toString()
{
return (string) $this->getPath();
}
}
and this is the couleur type
class CouleurType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nom')
->add('codeCouleur')
->add(
'images', CollectionType::class,
array(
'label' => 'Images de l\'article ayant cette couleur',
'entry_type' => ImageNewFileType::class,
'allow_add' => true,
'allow_delete' => true,
)
);
}
}
and finally the image type
class ImageNewFileType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'file', FileType::class,
[
'mapped' => false,
'required' => false,
'attr' => array(
'accept' => 'image/*',
)
]
);
}
}
I think you shouldn't add the mapped => false option in the ImageNewFileType.
https://symfony.com/doc/current/reference/forms/types/form.html#mapped
As you can see in the documentation the field is ignored when writing to the object.
the error was inside the ImageNewFileType, because of property 'mapped' => false, the form wasn't set uploaded files information in file field of ImageNew Entity, so I've replaced this :
class ImageNewFileType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'file', FileType::class,
[
'mapped' => false,
'required' => false,
'attr' => array(
'accept' => 'image/*',
)
]
);
}
}
by this:
class ImageNewFileType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'file', FileType::class,
[
'mapped' => true(or live this empty because by default it is true),
'required' => false,
'attr' => array(
'accept' => 'image/*',
)
]
);
}
}
I work on a OneToMany association in my database. This association work perfectly when I try to add data from fixtures and when I try to return data from database.
The problem is with my FormType CommandType which does'nt work. Symfony and Doctrine return this error message :
An exception occurred while executing 'INSERT INTO command_product (quantity, command_id, product_id) VALUES (?, ?, ?)' with params [3, null, 1]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'command_id' cannot be null
CommandType's code :
class CommandType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('establishment', EntityType::class, array(
'class' => Company::class,
'required' => true
))
->add('dateCreation', DateTimeType::class, array(
'widget' => 'single_text',
'format' => 'yyyy-MM-dd',
'required' => true
))
->add('contains', CollectionType::class, array(
'entry_type' => CommandProductType::class,
'required' => true,
'allow_add' => true
))
->add('state',TextType::class, array(
'required' => true
))
->add('totalAmount', MoneyType::class, array(
'required' => true
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Command::class
));
}
public function getBlockPrefix()
{
return 'appbundle_command';
}
}
CommandProductType's code :
class CommandProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('quantity', NumberType::class, array(
'required' => true
))
->add('product', EntityType::class, array(
'class' => Product::class,
'required' => true
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => CommandProduct::class
));
}
public function getBlockPrefix()
{
return 'appbundle_commandproduct';
}
}
Command's code class :
class Command
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Company $establishment
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Company")
* #ORM\JoinColumn(name="establishment_id", referencedColumnName="id", nullable=false)
*/
private $establishment;
/**
* #var DateTime $dateCreation
*
* #ORM\Column(name="dateCreation", type="datetime", nullable=false)
* #Assert\Type("datetime")
*/
private $dateCreation;
/**
* #var string $state
*
* #ORM\Column(name="state", type="string", length=255, nullable=false)
* #Assert\Type("string")
*/
private $state;
/**
* #var float $totalAmount
*
* #ORM\Column(name="totalAmount", type="float", precision=10, scale=2, nullable=false)
* #Assert\NotBlank()
* #Assert\Type(type="float")
*/
private $totalAmount;
/**
* #var mixed $contains
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\CommandProduct", mappedBy="contain", cascade={"persist", "remove"})
*/
private $contains;
public function __construct()
{
$this->contains = new ArrayCollection();
}
/**
* #var CommandProduct $commandProduct
*/
public function addContain(CommandProduct $commandProduct = null)
{
$commandProduct->setContain($this);
$this->contains->add($commandProduct);
}
/**
* #param CommandProduct $commandProduct
*/
public function removeContain(CommandProduct $commandProduct)
{
if ($this->contains->contains($commandProduct)) {
$this->contains->removeElement($commandProduct);
}
}
}
CommandOrder's code class :
class CommandProduct
{
/**
* #var int $id
*
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var Command $contain
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Command", inversedBy="contains")
* #ORM\JoinColumn(name="command_id", referencedColumnName="id", nullable=false)
*/
private $contain;
/**
* #var int $quantity
*
* #ORM\Column(name="quantity", type="integer", nullable=true, options={"default": 1})
* #Assert\NotBlank()
* #Assert\Type(type="int")
*/
private $quantity;
/**
* #var Product $product
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Product")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
}
I'm learning by myself the symfony framework (my job is not about developing, I'm not a developer) and I find out most of case the solution but here, is one what I didn't know how to manage.
I have 2 entity :
Product:
/**
* Product
*
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\ProductRepository")
* #UniqueEntity("productNumber")
*/
class Product
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="productNumber", type="string", length=255, unique=true)
* #Assert\Regex(
* pattern="/[0-9][.][0-9]{3}/",
* message="It should be like 1.234"
* )
*/
private $productNumber;
/**
* #ORM\ManyToOne(targetEntity="ProductGroup")
*/
private $productGroup;
/**
* Constructor
*/
public function __construct()
{
}
}
Camera :
/**
* Camera
*
* #ORM\Table(name="camera")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\CameraRepository")
*/
class Camera
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
*/
private $description;
/**
*
* #ORM\ManyToOne(targetEntity="Product")
*/
private $product;
/**
* #ORM\ManyToMany(targetEntity="CustomField", inversedBy="camera", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $customFields;
/**
* Constructor
*/
public function __construct()
{
$this->customFields = new ArrayCollection();
}
}
My form :
namespace ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
class CameraType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('product', EntityType::class, [
'class' => 'ProductBundle:Product',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
},
'attr' => [
'required' => true,
],
'choice_label' => 'productNumber',
])
->add('modele', TextType::class, [
'label' => "Modele",
])
->add('description', TextType::class, [
'label' => "Description",
])
->add('customFields', CollectionType::class, [
'entry_type' => CustomFieldType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => false,
'attr' => [
'class' => 'customfield'
]
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ProductBundle\Entity\Camera'
));
}
}
When I add a camera, I would like only the Product:productNumber where are available (not take by a camera), the querybuilder is working but my issue concern the edit form, it show only available productNumber so it's changing every time I need to edit this camera.
What can I handle this ? Should I try to found another way to add a productNumber ? do you have a "trick" ?
I hope you will understand the problem and my english because it's not my first language.
Have a nice day.
Edit : I'm on Symfony 3.1.4
I presume on new form your choice field shows only unused ProductBundle:Camera entity, and on edit form it should show saved ProductBundle:Camera entity and all unused ones.
You should look into Form Event Subscribers
You need to implement two event listeners PRE_SET_DATA and PRE_SUBMIT.
Here is one way to do it. Something like this works on SF 2.8
First you will have to create product entity form from custom ProductFieldSubscriber which becomes EventSubscriberInterface:
$builder->addEventSubscriber(new ProductFieldSubscriber('product', [])
Now ProductFieldSubscriber should look something like this (untested)
namespace ProductBundle\Form\EventListener;
use Symfony\Component\Form\FormInterface,
Symfony\Component\Form\FormEvent,
Symfony\Component\EventDispatcher\EventSubscriberInterface,
Symfony\Component\Form\FormEvents,
Doctrine\ORM\EntityRepository,
Symfony\Bridge\Doctrine\Form\Type as DoctrineTypes
;
class ProductFieldSubscriber implements EventSubscriberInterface
{
private $propertyPathToSelf;
public function __construct($propertyPathToSelf, array $formOptions=[]) {
$this->propertyPathToSelf = $propertyPathToSelf;
$this->formOptions = $formOptions;
}
public static function getSubscribedEvents() {
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit',
];
}
private function addForm(FormInterface $form, $selfId = null) {
$formOptions = array_replace_recursive ([
'class' => 'ProductBundle:Product',
'placeholder' => null,
'compound' => false,
'query_builder' => function (EntityRepository $er) use ($selfId) {
$qb = $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
if (null !== $selfId) {
$qb
->orWhere($qb->expr()->eq('p.product', ':existingId'))
->setParameter('existingId', $selfId->getId())
;
}
return $qb;
},
],
$this->formOptions
);
if ($selfId) {
$formOptions['data'] = $selfId;
}
$form->add($this->propertyPathToSelf, DoctrineTypes\EntityType::class, $formOptions);
}
public function onPreSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$selfIdTypeMethod = "get{$this->propertyPathToSelf}";
$selfId = $data->$selfIdTypeMethod();
$this->addForm($form, $selfId);
}
public function onPreSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$selfId = array_key_exists($this->propertyPathToSelf, $data) ? $data[$this->propertyPathToSelf] : null;
$this->addForm($form, $selfId);
}
}
Query builder would be simpler if you had mapped entity relations.
Bonus update:
form option 'placeholder' => null, takes care that no default 'empty' option is available.
form option 'required' => true, forces html5 form popup validation.
Then you should use something like entity #assert notations and use validator constraints on entity attribute:
use Symfony\Component\Validator\Constraints as Assert;
/**
* #var string
*
* #Assert\NotNull()
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
You could also disallow edit form from opening via controller editAction (maybe some redirect) and twig, where you could hide edit button.
I have problem with form collection. I get an error:
Notice: Array to string conversion in /home/.../vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php line 103
i tried foreach (like is in doctrine documentation) and perist each object, but I get an error:
The class 'Doctrine\Common\Collections\ArrayCollection' was not found in the chain configured namespaces FOS\UserBundle\Entity, Ix\UserBundle\Entity, Ix\x\Entity, FOS\UserBundle\Model
Below is my code:
Entity:
/**
* #ORM\Entity
* #ORM\Table(name="availability")
*/
class Availability
{
public function __construct()
{
$this->availabilityFlexible = new \Doctrine\Common\Collections\ArrayCollection();
$this->availabilitySession = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="description", type="string", length=255, nullable=true)
*/
protected $description;
/**
* #ORM\OneToMany(targetEntity="AvailabilityFlexible", mappedBy="availability", cascade={"persist", "remove"})
*/
protected $availabilityFlexible;
/**
* #ORM\OneToOne(targetEntity="AvailabilityFull", mappedBy="availability", cascade={"persist", "remove"})
*/
protected $availabilityFull;
/**
* #ORM\OneToMany(targetEntity="AvailabilitySession", mappedBy="availability", cascade={"persist", "remove"})
*/
protected $availabilitySession;
// AvailabilityFlexible.php
/**
* #ORM\ManyToOne(targetEntity="Availability", inversedBy="availabilityFlexible", cascade={"persist"})
* #ORM\JoinColumn(name="availability_id", referencedColumnName="id")
*/
protected $availability;
// AvailabilityFull.php
/**
* #ORM\OneToOne(targetEntity="Availability", inversedBy="availabilityFull", cascade={"persist"})
* #ORM\JoinColumn(name="availability_id", referencedColumnName="id")
*/
protected $availability;
// AvailabilitySession.php
/**
* #ORM\ManyToOne(targetEntity="Availability", inversedBy="availabilitySession", cascade={"persist"})
* #ORM\JoinColumn(name="availability_id", referencedColumnName="id")
*/
protected $availability;
Form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description')
->add('AvailabilitySession', 'collection', array(
'type' => new AvailabilitySessionFormType(),
'allow_add' => true,
'prototype' => true,
'by_reference' => false,
'allow_delete' => true,
))
->add('AvailabilityFull', new AvailabilityFullFormType())
->add('AvailabilityFlexible', 'collection', array(
'type' => new AvailabilityFlexibleFormType(),
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
));
}
Controller:
public function addAvailabilityAction(Request $request)
{
$availability = new Availability;
$availability->getAvailabilityFlexible()->add(new AvailabilityFlexible);
$availability->getAvailabilityFlexible()->add(new AvailabilityFlexible);
$form = $this->createForm(new AvailabilityFormType(), $availability);
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$availability = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->perist($availability);
$em->flush();
}
}
}
Properties $availability are in different class.
AvailabilityFlexible.php
AvailabilityFull.php
AvailabilitySession.php
This is relationship to Availability Entity.