Symfony/Doctrine - Create/update entity with OneToMany - php

first of all, i'm sorry if that double post but I don't found any ressource with this issue or exemple that fix it and i don't understand what's going in his head.
I'm in Symfony/Doctrine and Vue/Nuxt on front,
So GOAL :
Update an array of entity (AideSection) where one prop is OneToMany for another entity (AideSubSection::AideSection)
Here exemple of the structure of my item :
[
{
id: 0,
isActive: true,
libelle: "",
ordered: 0,
aideSubSection: [
{
id: 0,
isActive: true,
isForDashboard: true,
ordered: 0,
orderDashboard: 0,
aideSection: 0,
libelle: "",
content: ""
}
]
}
]
If I modify only data return by api and save , no problem, BUT when I try to save with new item, here is (partial) content of my post request:
enter image description here
So my ids are null, and here my controller and another file
#[Route("/backoffice/aide/save", name: "save_aide", methods: ["POST"])]
public function save(Request $request, AideSectionManager $manager) :JsonResponse
{
$post = $request->request->all();
/** #var AideSection $data */
foreach ($post as &$data) {
if (!empty($data['id'])) {
$section = $manager->find($data['id']);
} else {
$section = new AideSection();
}
$form = $this->createForm(AideSectionFormType::class, $section);
$form->submit($data);
if (!$form->isSubmitted() || !$form->isValid()) {
throw new Exception('Error');
}
$this->em->persist($section);
}
$this->em->flush();
return self::response('ok');
}
Here my FormType
For AideSection
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('libelle', TextType::class)
->add('isActive', CheckboxType::class)
->add('ordered', IntegerType::class)
->add('aideSubSection', CollectionType::class, [
'entry_type' => AideSubSectionFormType::class,
'allow_delete' => true,
'allow_add' => true
]);
}
For AideSubSection
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('libelle', TextType::class)
->add('content', TextType::class)
->add('isActive', CheckboxType::class)
->add('isForDashboard', CheckboxType::class)
->add('ordered', IntegerType::class)
->add('orderDashboard', IntegerType::class);
}
My Entity (with relation)
class AideSection
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
#[Groups('default')]
private $id;
#[ORM\Column]
#[Groups('default')]
private bool $isActive;
#[ORM\Column]
#[Groups('default')]
private string $libelle;
#[ORM\Column]
#[Groups('default')]
private int $ordered;
#[ORM\OneToMany(mappedBy: "aideSection", targetEntity: AideSubSection::class, cascade:
["persist"], fetch: "EAGER")]
#[Groups('default')]
private Collection $aideSubSection;
#[Pure] public function __construct()
{
$this->aideSubSection = new ArrayCollection();
}
class AideSubSection
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
#[Groups('default')]
private $id;
#[ORM\Column]
#[Groups('default')]
private string $libelle;
#[ORM\Column(type: Types::TEXT)]
#[Groups('default')]
private string $content;
#[ORM\Column]
#[Groups('default')]
private bool $isForDashboard;
#[ORM\Column]
#[Groups('default')]
private int $orderDashboard;
#[ORM\Column]
#[Groups('default')]
private int $ordered;
#[ORM\Column]
#[Groups('default')]
private bool $isActive;
#[ORM\ManyToOne(targetEntity: AideSection::class, inversedBy: "aideSubSection"),
ORM\JoinColumn(nullable: false)]
#[Groups('default')]
private AideSection $aideSection;
Same problem on my AideSection, I don't understand why it doesn't detect or update ids from my AideSetion or AideSubSection :/

Get the data from Form right after you checked if submitted and valid.
Then check if collection of aideSubSection isn't empty.
If it's not, loop through all, check if "new" (id is null) and persist.
if (!$form->isSubmitted() || !$form->isValid()) {
throw new Exception('Error');
}
$section = $form->getData(); // gives you instance of AideSection
// dd($section); // check data
// loop through all
foreach($section->getAideSubSection() as $aideSubSection){
// if "new"
if( $aideSubSection->getId === null ){
$this->em->persist($aideSubSection);
}
// make sure they are "linked"
$aideSubSection->setAideSection($section);
}
// persist only if $section is also "new"
if( $section->getId() === null ){
$this->em->persist($section);
}
$this->em->flush();

Related

Symfony 6 FormType - How to get parameter from Object inside Object

I use Symfony 6 and I'm trying to get parameter - list of values (ChoiceType) from Object -> Prodline inside another Object -> Logging (relation in the database between objects) when I try create new element.
I generated Entities by command "make:entity" and using Doctrine, I generated a CRUD of them (Prodline and Logging - relation OneToMany - 1 logging can have 1 prodline, but 1 prodline can have many loggings).
Prodline has some data in the database and now I want to select a parameter/column and display it as a ChoiseType/List inside a view to create a new login.
I know I can use magic method - __toString() inside Prodline but i need several parameters in different views and I don't know how can I get/choose parameters what I want.
Prodline.php
#[ORM\Entity(repositoryClass: ProdlineRepository::class)]
class Prodline
{
...
#[ORM\Column(length: 80, nullable: true)]
private ?string $prodlinename = null;
#[ORM\Column(length: 255)]
private ?string $infoText = null;
#[ORM\OneToMany(mappedBy: 'prodline', targetEntity: Logging::class)]
private Collection $loggings;
public function __construct()
{
$this->loggings = new ArrayCollection();
}
/**
* #return Collection<int, Logging>
*/
public function getLoggings(): Collection
{
return $this->loggings;
}
...
}
Logging.php
#[ORM\Entity(repositoryClass: LoggingRepository::class)]
class Logging
{
...
#[ORM\Column(length: 8)]
private ?string $user = null;
#[ORM\ManyToOne(targetEntity: Prodline::class, inversedBy: 'loggings')]
#[ORM\JoinColumn(name: 'prodline_id', referencedColumnName: 'id', nullable: false)]
private ?Prodline $prodline = null;
...
public function getProdline(): ?Prodline
{
return $this->prodline;
}
public function setProdline(?Prodline $prodline): self
{
$this->prodline = $prodline;
return $this;
}
...
And now what generated CRUD:
LoggingController.php
#[Route('/logging')]
class LoggingController extends AbstractController
{
...
#[Route('/new', name: 'app_logging_new', methods: ['GET', 'POST'])]
public function new(Request $request, LoggingRepository $loggingRepository): Response
{
$logging = new Logging();
$form = $this->createForm(LoggingType::class, $logging);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$loggingRepository->save($logging, true);
return $this->redirectToRoute('app_logging_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('logging/new.html.twig', [
'logging' => $logging,
'form' => $form,
]);
}
...
}
FormType for create new logging:
LoggingType.php
class LoggingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('user')
->add('prodline')
...
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Logging::class,
]);
}
}
Templates html.twig:
new.html.twig
{% extends 'base.html.twig' %}
{% block body %}
{{ include('logging/_form.html.twig') }}
{% endblock %}
_form.html.twig
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ form_end(form) }}
I tried many things like:
1.Added ProdlineRepository to LoggingController and findAll() records and get Prodlinename parameter as array.
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
...
#[Route('/new', name: 'app_logging_new', methods: ['GET', 'POST'])]
public function new(Request $request, LoggingRepository $loggingRepository): Response
{
$logging = new Logging();
$prodlineRepository = $this->em->getRepository(Prodline::class);
$prodline = $prodlineRepository->findAll();
$logging->setProdlineName($logging->getProdlineNameArray($prodline));
$form = $this->createForm(LoggingType::class, $logging);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$loggingRepository->save($logging, true);
return $this->redirectToRoute('app_logging_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('logging/new.html.twig', [
'logging' => $logging,
'form' => $form,
]);
}
Then added an extra parameter (array prodlineName) in Logging.php to get list of prodlineNames from Prodline.php (got 3 elements) and add this parameter to LoggingType but I get Errors like "Can't displey array as a string" ect.
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('user')
->add('prodlineName')
...
}
Tried add options in LoggingType like:
$builder
->add('user')
->add('prodlineName', EntityType::class, [
'class' => Prodline::class,
'multiple' => true,
}
])
// ->add('prodline', EntityType::class, [
// 'class' => Prodline::class,
// ])
// ->add('prodline', ChoiceType::class, [
// 'choice_value' => function (?Logging $entity) {
// return $entity ? $entity->getProdline()->getProdlinename() : '';
// }
// // 'choice_value' => 'prodlineName'
// ])
// ->add('prodlineName', ChoiceType::class)

SOLVED Symfony 6 / PropertyAccessor requires a graph of objects or arrays to operate on, but found type "NULL" while trying to traverse path

I require some help, I'm facing this problem since one month and try to solve it (already searched on many forums but nothing worked).
Here is my error screen
PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "NULL" while trying to traverse path "animal.nom" at property "nom".
I use Vich Uploader with a directory namer and it's seems like it doesnt get the value of my Entity to get the path.
The error only appear when I delete an image of my Entity, but on upload there's no problem.
Here's my entity file of my EntityImage (named: AnimalImage)
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\AnimalImageRepository;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
#[ORM\Entity(repositoryClass: AnimalImageRepository::class)]
#[Vich\Uploadable]
class AnimalImage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'animalImages', cascade: ['persist'])]
#[ORM\JoinColumn(nullable: false)]
private ?Animal $animal = null;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*/
#[Vich\UploadableField(mapping: 'animal', fileNameProperty: 'imageName', size: 'imageSize')]
private ?File $imageFile = null;
#[ORM\Column(length: 500)]
private ?string $imageName = null;
#[ORM\Column(length: 500)]
private ?string $imageSize = null;
#[ORM\Column]
private ?\DateTimeImmutable $updatedAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getAnimal(): ?Animal
{
return $this->animal;
}
public function setAnimal(?Animal $animal): self
{
$this->animal = $animal;
return $this;
}
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageName(string $imageName): self
{
$this->imageName = $imageName;
return $this;
}
public function getImageSize(): ?int
{
return $this->imageSize;
}
public function setImageSize(int $imageSize): self
{
$this->imageSize = $imageSize;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeImmutable $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get the value of imageFile
*/
public function getImageFile(): ?File
{
return $this->imageFile;
}
/**
* Set the value of imageFile
*
* #return self
*/
public function setImageFile(?File $imageFile = null): void
{
$this->imageFile = $imageFile;
if(null !== $imageFile){
$this->updatedAt = new \DateTimeImmutable();
}
}
}
Here is my form type for my Animal Entity
namespace App\Form;
use App\Entity\Race;
use App\Entity\Tags;
use App\Entity\Animal;
use App\Entity\Espece;
use App\Form\AnimalImageType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\AbstractType;
use Doctrine\ORM\EntityRepository;
use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class AnimalType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('nom', TextType::class, [
"label"=>"Nom : ",
"required"=>true
])
->add('genre', ChoiceType::class, [
"choices"=> [
'Femelle'=>'Femelle',
'Mâle'=>'Male',
],
"label"=>"Genre : ",
"required"=>true,
"expanded"=>true,
])
->add('entente_chien', ChoiceType::class, [
"choices"=> [
'Oui'=>'1',
'Non'=>'0',
"N/A"=>"2",
],
"label"=>"Entente avec les chiens : ",
"required"=>true,
// "expanded"=>true,
])
->add('entente_chat', ChoiceType::class, [
"choices"=> [
'Oui'=>'1',
'Non'=>'0',
"N/A"=>"2",
],
"label"=>"Entente avec les chats : ",
"required"=>true,
// "expanded"=>true
])
->add('entente_enfant', ChoiceType::class, [
"choices"=> [
'Oui'=>'1',
'Non'=>'0',
"N/A"=>"2",
],
"label"=>"Entente avec les enfants : ",
"required"=>true,
// "expanded"=>true
])
->add('titre', TextareaType::class, [
"label"=>"Titre : ",
"required"=>true,
])
->add('description', CKEditorType::class, [
"label"=>'Description : ',
"required"=>true,
])
->add('Espece', EntityType::class, [
"class" => Espece::class,
"label" => "Espèce : ",
"required" => true,
"choice_label" => "nom",
"placeholder"=>"----",
// "empty_data"=>null,
// "by_reference" => false
])
->add('race', EntityType::class,[
"class"=> Race::class,
"label"=>"Race : ",
"required"=>true,
"choice_label" => 'nom',
"choice_attr"=>ChoiceList::attr($this, function(?Race $race){
return $race ? ['data-espece'=>$race->getEspeceId()] : [];
}),
"placeholder"=>"----",
// "empty_data"=>null,
])
->add('tags', EntityType::class, [
'class' => Tags::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c');
},
'label' => 'Caractéristiques :',
'expanded' => true,
'multiple' => true,
'choice_label' => 'caracteristique',
'by_reference' => false,
"required"=>true,
"empty_data"=>null,
])
->add('age', ChoiceType::class, [
"label"=>"Âge : ",
"placeholder"=>"----",
'choices'=> [...]
],
"required"=>true,
])
->add('enteredAt', DateType::class, [
'label'=>'Date d\'entrée : ',
'widget'=>'choice',
'input'=>'datetime_immutable',
'format'=>'dd / MM / yyyy',
'required'=>true,
]);
// If the animal is not created
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
// Récupère les données du formulaire si l'article est déjà rempli
$animal = $event->getData();
// Récupère le formulaire
$form = $event->getForm();
// Is the animal already exist
if(!$animal || null === $animal->getId()){
$form->add('animalImages', CollectionType::class, [
'entry_type'=>AnimalImageType::class,
'allow_add'=>true,
'allow_delete'=>true,
'delete_empty'=>true,
'prototype'=>true,
'by_reference'=>false,
'mapped'=>false,
]);
} else {
$form->add('animalImages', CollectionType::class, [
'entry_type'=>AnimalImageType::class,
'allow_add'=>true,
'allow_delete'=>true,
'delete_empty'=>true,
'prototype'=>true,
'by_reference'=>false,
]);
}
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Animal::class,
]);
}
public function preUpdate(object $animal): void
{
$this->animalManager->updateCanonicalFields($animal);
}
}
and my mapping on vich_uploader.yaml
animal:
uri_prefix: /images/animaux
upload_destination: "%kernel.project_dir%/public/images/animaux"
directory_namer:
service: vich_uploader.namer_directory_property
options: { property: "animal.nom", transliterate: true }
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
inject_on_load: false
delete_on_update: true
delete_on_remove: true
And finally, my Entity file (named: Animal)
<?php
namespace App\Entity;
use App\Entity\AnimalImage;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\AnimalRepository;
use Vich\UploaderBundle\Entity\File;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: AnimalRepository::class)]
class Animal
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'animals')]
#[ORM\JoinColumn(nullable: false)]
private ?Espece $espece = null;
#[ORM\ManyToOne(inversedBy: 'animals')]
#[ORM\JoinColumn(nullable: false)]
private ?Race $race = null;
#[ORM\Column(length: 50)]
private ?string $nom = null;
#[ORM\Column(length: 30)]
private ?string $genre = null;
#[ORM\Column(nullable: true)]
private ?int $entente_chien = null;
#[ORM\Column(nullable: true)]
private ?int $entente_chat = null;
#[ORM\Column(nullable: true)]
private ?int $entente_enfant = null;
#[ORM\Column(length: 200, nullable: true)]
#[Assert\Length(
max: 38,
maxMessage: 'Le titre ne peut excéder {{limit}} caractères',
)]
private ?string $titre = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $description = null;
#[ORM\OneToMany(mappedBy: 'animal', targetEntity: AnimalImage::class, orphanRemoval: true, cascade: ['persist'])]
private Collection $animalImages;
#[ORM\Column(length: 100)]
private ?string $age = null;
#[ORM\Column]
private ?\DateTimeImmutable $enteredAt = null;
#[ORM\ManyToMany(targetEntity: Tags::class, inversedBy: 'animals')]
private Collection $tags;
public function __construct()
{
$this->animalImages = new ArrayCollection();
$this->tags = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEspece(): ?Espece
{
return $this->espece;
}
public function setEspece(?Espece $espece): self
{
$this->espece = $espece;
return $this;
}
public function getRace(): ?race
{
return $this->race;
}
public function setRace(?race $race): self
{
$this->race = $race;
return $this;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
public function getGenre(): ?string
{
return $this->genre;
}
public function setGenre(string $genre): self
{
$this->genre = $genre;
return $this;
}
public function getEntenteChien(): ?int
{
return $this->entente_chien;
}
public function setEntenteChien(?int $entente_chien): self
{
$this->entente_chien = $entente_chien;
return $this;
}
public function getEntenteChat(): ?int
{
return $this->entente_chat;
}
public function setEntenteChat(?int $entente_chat): self
{
$this->entente_chat = $entente_chat;
return $this;
}
public function getEntenteEnfant(): ?int
{
return $this->entente_enfant;
}
public function setEntenteEnfant(?int $entente_enfant): self
{
$this->entente_enfant = $entente_enfant;
return $this;
}
public function getTitre(): ?string
{
return $this->titre;
}
public function setTitre(?string $titre): self
{
$this->titre = $titre;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
/**
* #return Collection<int, AnimalImage>
*/
public function getAnimalImages(): Collection
{
return $this->animalImages;
}
/**
* Set the value of animalImages
*
* #return self
*/
public function setAnimalImages(?File $animalImages = null): void
{
$this->animalImages = $animalImages;
if(null!==$animalImages){
$this->updatedAt = new \DateTimeImmutable();
}
}
public function addAnimalImage(AnimalImage $animalImage): self
{
if (!$this->animalImages->contains($animalImage)) {
$this->animalImages[] = $animalImage;
$animalImage->setAnimal($this);
}
return $this;
}
public function removeAnimalImage(AnimalImage $animalImage): self
{
if ($this->animalImages->removeElement($animalImage)) {
// set the owning side to null (unless already changed)
if ($animalImage->getAnimal() === $this) {
$animalImage->setAnimal(null);
}
}
return $this;
}
public function getAge(): ?string
{
return $this->age;
}
public function setAge(string $age): self
{
$this->age = $age;
return $this;
}
public function getEnteredAt(): ?\DateTimeImmutable
{
return $this->enteredAt;
}
public function setEnteredAt(\DateTimeImmutable $enteredAt): self
{
$this->enteredAt = $enteredAt;
return $this;
}
/**
* #return Collection<int, Tags>
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tags $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
}
return $this;
}
public function removeTag(Tags $tag): self
{
$this->tags->removeElement($tag);
return $this;
}
}
Thanks in advance for any solution and help, worked on this project 1month and worked on this error during long days :/
EDIT : Tried a lot of "solutions" and commented a single line and now it works.
Here's my solution
{
if ($this->animalImages->removeElement($animalImage)) {
// set the owning side to null (unless already changed)
if ($animalImage->getAnimal() === $this) {
$fileName = $animalImage->getImageName();
$filesystem = new Filesystem();
$filesystem->remove('%kernel.project_dir%/public/images/animaux/'.$this->getNom().'/'. $fileName);
// COMMENTING THIS LINE MADE THE THING WORKS
// $animalImage->setAnimal(null);
}
}
return $this;
}

How to create a form with quantities linked with an EntityType field in Symfony 5?

I'm trying to recover some product and add a quantity field to the side for each of them.
All of this, in a promo code's formular.
PromoCode formular without product/quantity form row:
class CodePromoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('montantMinimum', MoneyType::class, [
'label' => 'Montant minimum en € requis pour appliquer la promotion',
'attr' => ['placeholder' => '10']
])
->add('montant', TextType::class, [
'label' => 'Montant en € ou %',
'attr' => ['placeholder' => '10']
])
->add('DLC', DateType::class, [
'label' => 'Date limite de consomation',
'attr' => ['class' => 'form-control']
])
->add('fraisDePortOfferts', CheckboxType::class, [
'label' => 'Frais de port offert',
'required' => false,
'attr' => ['class' => 'form-control']
])
->add('typePromo', ChoiceType::class, [
'label' => 'Type de promotion',
'choices' => [
'Euro €' => 'e',
'Pourcentage %' => 'p',
],
'attr' => ['class' => 'form-control']
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => CodePromo::class,
'allow_extra_fields' => true
]);
}
}
I'm using the products's relation (oneToMany) in my PromoCode class to create my form with the form builder.
#[ORM\Entity(repositoryClass: CodePromoRepository::class)]
class CodePromo
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 20)]
private $code;
#[ORM\Column(type: 'datetime')]
private $DLC;
#[ORM\Column(type: 'datetime')]
private $dateC;
#[ORM\Column(type: 'float')]
private $montant;
#[ORM\Column(type: 'string', length: 1)]
private $typePromo;
#[ORM\Column(type: 'float')]
private $montantMinimum;
#[ORM\OneToMany(mappedBy: 'codePromo', targetEntity: ProduitCodePromo::class)]
private $produits;
#[ORM\Column(type: 'boolean')]
private $fraisDePortOfferts;
public function __construct()
{
$this->produits = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getDLC(): ?\DateTimeInterface
{
return $this->DLC;
}
public function setDLC(\DateTimeInterface $DLC): self
{
$this->DLC = $DLC;
return $this;
}
public function getDateC(): ?\DateTimeInterface
{
return $this->dateC;
}
public function setDateC(\DateTimeInterface $dateC): self
{
$this->dateC = $dateC;
return $this;
}
public function getMontant(): ?float
{
return $this->montant;
}
public function setMontant(float $montant): self
{
$this->montant = $montant;
return $this;
}
public function getTypePromo(): ?string
{
return $this->typePromo;
}
public function setTypePromo(string $typePromo): self
{
$this->typePromo = $typePromo;
return $this;
}
public function getMontantMinimum(): ?float
{
return $this->montantMinimum;
}
public function setMontantMinimum(float $montantMinimum): self
{
$this->montantMinimum = $montantMinimum;
return $this;
}
/**
* #return Collection<int, ProduitCodePromo>
*/
public function getProduit(): Collection
{
return $this->produits;
}
public function addProduit(ProduitCodePromo $produits): self
{
if (!$this->produits->contains($produits)) {
$this->produits[] = $produits;
$produits->setCodePromo($this);
}
return $this;
}
public function removeProduit(ProduitCodePromo $produits): self
{
if ($this->produits->removeElement($produits)) {
// set the owning side to null (unless already changed)
if ($produits->getCodePromo() === $this) {
$produits->setCodePromo(null);
}
}
return $this;
}
public function getFraisDePortOfferts(): ?bool
{
return $this->fraisDePortOfferts;
}
public function setFraisDePortOfferts(bool $fraisDePortOfferts): self
{
$this->fraisDePortOfferts = $fraisDePortOfferts;
return $this;
}
}
This is the table linked with PromoCode, as u can see, I want to use this quantity field.
#[ORM\Entity(repositoryClass: ProduitCodePromoRepository::class)]
class ProduitCodePromo
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\ManyToOne(targetEntity: Produit::class, inversedBy: 'produitsCodePromo')]
#[ORM\JoinColumn(nullable: false)]
private $produit;
#[ORM\ManyToOne(targetEntity: CodePromo::class)]
#[ORM\JoinColumn(nullable: false)]
private $codePromo;
#[ORM\Column(type: 'integer')]
private $quantite;
public function getId(): ?int
{
return $this->id;
}
public function getProduit(): ?Produit
{
return $this->produit;
}
public function setProduit(?Produit $produit): self
{
$this->produit = $produit;
return $this;
}
public function getCodePromo(): ?CodePromo
{
return $this->codePromo;
}
public function setCodePromo(?CodePromo $codePromo): self
{
$this->codePromo = $codePromo;
return $this;
}
public function getQuantite(): ?int
{
return $this->quantite;
}
public function setQuantite(int $quantite): self
{
$this->quantite = $quantite;
return $this;
}
}
You'll have to create a new FormType for your ProduitCodePromo entity and add it to your base form. You'll have to also add a product field in your new formType.
The way it should be added is described in a lot of other answers here. In that case you'll have to use the CollectionType. You'll find an (with ManyToMany relationship) exemple here Symfony 5 Many to Many with extra fields form builder

Embedding a single object in form

I've manage to create a form embedded in another form but I think I'm not doing something right. Here's my code
Category
class Category
{
private $id;
private $name;
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="category")
*/
private $subcategorues;
public function __construct()
{
$this->subcategorues = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function addSubcategorue(\AppBundle\Entity\Category $subcategorues)
{
$this->subcategorues[] = $subcategorues;
return $this;
}
public function removeSubcategorue(\AppBundle\Entity\Category $subcategorues)
{
$this->subcategorues->removeElement($subcategorues);
}
public function getSubcategorues()
{
return $this->subcategorues;
}
}
Subcategory
class Subcategory
{
private $id;
private $name;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="subcategories")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
/**
* #return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* #param mixed $category
*/
public function setCategory($category)
{
$this->category = $category;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
}
CategoryType
.......
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'entity', [
'class' => 'AppBundle\Entity\Category',
'choice_label' => 'name'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Category'
]);
}
......
SubcategoryType
$builder
->add('category', new CategoryType(), [
'label' => false
])
->add('name', 'text')
->add('save', 'submit')
;
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Subcategory'
]);
}
DefaultController
public function indexAction(Request $request)
{
$subcategory = new Subcategory();
$form = $this->createForm(new SubcategoryType(), $subcategory);
$form->handleRequest($request);
if($form->isValid()){
$em = $this->getDoctrine()->getManager();
$subcategory->setCategory($subcategory->getCategory()->getName());
$em->persist($subcategory);
$em->flush();
return new Response(sprintf('ID %d', $subcategory->getId()));
}
return $this->render('AppBundle::layout.html.twig', [
'form' => $form->createView(),
]);
}
Please notice this line of code $subcategory->setCategory($subcategory->getCategory()->getName());
I need that line in order to save the entity to the database otherwise I get an error. So my question is is there a way to skip this line of code and pass category object on the fly to subcategory->category property instead of doing that manually?
//EDIT
Here's the output of dump($form->getData());
DefaultController.php on line 33:
Subcategory {#467 ▼
-id: null
-name: "Uncharted"
-category: Category {#588 ▼
-id: null
-name: Category {#685 ▼
-id: 2
-name: "Games"
-subcategorues: PersistentCollection {#686 ▶}
}
-subcategorues: ArrayCollection {#660 ▶}
}
}
Your CategoryType is not correctly mapped compared to your Category entity. Actually, in your case, you don't need to have a sub-form CategoryType with a name field, since you have a category field in SubCategory which is a relationship towards Category.
Just replace:
->add('category', new CategoryType(), [
'label' => false
])
by:
->add('category', 'entity', [
'class' => 'AppBundle\Entity\Category',
'choice_label' => 'name'
]);
Could your try smth like this (for Category entity class):
public function addSubcategorue(\AppBundle\Entity\Category $subcategorues)
{
if ($this->subcategorues->contains($subcategorues)) {
$this->subcategorues->add($subcategorues);
$subcategorues->setCategory($this);
}
return $this;
}

Form edition for a N:M relationship with extra fields is not working

I'm working on a form that handle N:M relationship with an extra parameter (extra field/column). This is what I've done until now:
In OrdersType.php form:
class OrdersType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder fields
// $builder fields only need on edit form not in create
if ($options['curr_action'] !== NULL)
{
$builder
// other $builder fields
->add("orderProducts", "collection", array(
'type' => new OrdersHasProductType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Orders',
'render_fieldset' => FALSE,
'show_legend' => FALSE,
'intention' => 'orders_form',
'curr_action' => NULL
));
}
public function getName()
{
return 'orders';
}
}
In OrderHasProductType.php:
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
->add('amount', 'text', array(
'required' => TRUE,
'label' => FALSE
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\OrderHasProduct',
'intention' => 'order_has_product'
));
}
public function getName()
{
return 'order_has_product';
}
}
And finally this is Orders.php and OrdersHasProduct.php entities:
class Orders {
use IdentifiedAutogeneratedEntityTrait;
// rest of fields for the entity
/**
* #ORM\OneToMany(targetEntity="OrderHasProduct", mappedBy="order", cascade={"all"})
*/
protected $orderProducts;
protected $products;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function __construct()
{
$this->orderProducts = new ArrayCollection();
$this->products = new ArrayCollection();
}
public function getOrderProducts()
{
return $this->orderProducts;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
public function getProduct()
{
$products = new ArrayCollection();
foreach ($this->orderProducts as $op)
{
$products[] = $op->getProduct();
}
return $products;
}
public function setProduct($products)
{
foreach ($products as $p)
{
$ohp = new OrderHasProduct();
$ohp->setOrder($this);
$ohp->setProduct($p);
$this->addPo($ohp);
}
}
public function getOrder()
{
return $this;
}
public function addPo($ProductOrder)
{
$this->orderProducts[] = $ProductOrder;
}
public function removePo($ProductOrder)
{
return $this->orderProducts->removeElement($ProductOrder);
}
}
/**
* #ORM\Entity
* #ORM\Table(name="order_has_product")
* #Gedmo\SoftDeleteable(fieldName="deletedAt")
* #UniqueEntity(fields={"order", "product"})
*/
class OrderHasProduct {
use IdentifiedAutogeneratedEntityTrait;
/**
* Hook timestampable behavior
* updates createdAt, updatedAt fields
*/
use TimestampableEntity;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\FrontendBundle\Entity\Orders", inversedBy="orderProducts")
* #ORM\JoinColumn(name="general_orders_id", referencedColumnName="id")
*/
protected $order;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\ProductBundle\Entity\Product", inversedBy="orderProducts")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $amount;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function setOrder(\Tanane\FrontendBundle\Entity\Orders $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
public function setProduct(\Tanane\ProductBundle\Entity\Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
}
But when I try to edit a order with this code in my controller:
public function editAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('FrontendBundle:Orders')->find($id);
$type = $order->getPerson()->getPersonType() === 1 ? "natural" : "legal";
$params = explode('::', $request->attributes->get('_controller'));
$actionName = substr($params[1], 0, -6);
$orderForm = $this->createForm(new OrdersType(), $order, array('action' => '#', 'method' => 'POST', 'register_type' => $type, 'curr_action' => $actionName));
return array(
"form" => $orderForm->createView(),
'id' => $id,
'entity' => $order
);
}
I get this error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product. You can avoid this
error by setting the "data_class" option to
"Proxies__CG__\Tanane\ProductBundle\Entity\Product" or by adding a
view transformer that transforms an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product to scalar, array or
an instance of \ArrayAccess.
And I don't find or know how to fix it, can any give me some help?
I think this is happening because this code
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
));
}
//...
}
means that Symfony is expecting "product" to be a field of type "text", but when it calls getProduct() on the OrderHasProduct it gets a Product object (or a Doctrine Proxy to a Product, since it's not been loaded at that point). Symfony Fields inherit from Form/AbstractType, so they're essentially Forms in their own right, with just one field, hence the error message.
The solution is either to make that field of type "entity", or to create a different method which only gives the name of the Product, e.g. getProductName() on OrderHasProduct, and then use that as the data behind the field.

Categories