I have the entities: Students and StudyGroups
Everything works fine with Study Groups. I can choose the list of Students belongs to Study Groups, and Doctrine creates the associations.
However when I want to do the same thing from the other side (choose the Study Groups for students), nothing happens.
Here are the important parts of my YAML file configuration
StudyGroups.orm.yml
manyToMany:
students:
targetEntity: Students
inversedBy: studyGroups
cascade: ["persist"]
joinTable:
name: students_study_groups
joinColumns:
study_groups:
referencedColumnName: id
inverseJoinColumns:
students:
referencedColumnName: id
Students.orm.yml
manyToMany:
studyGroups:
targetEntity: StudyGroups
mappedBy: students
cascade: ["persist"]
lifecycleCallbacks: { }
I also tried the following modification in StudyGroups.php entity file
public function addStudent(\AppBundle\Entity\Students $students) {
$students->addStudyGroup($this); // ADDED THIS LINE
$this->students[] = $students;
return $this;
}
Same in Students.php entity file
public function addStudyGroup(\AppBundle\Entity\StudyGroups $studyGroups) {
$studyGroups->addStudent($this); // ADDED THIS LINE
$this->studyGroups[] = $studyGroups;
return $this;
}
The interesting thing I found about this problem, is that if I switch the Inverse and Owning side in StudyGroups.orm.yml and Students.orm.yml the issue will switch too.
I am aware that this is mentioned here: http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/unitofwork-associations.html
11.2. Important concepts
Changes made only to the inverse side of an association are ignored. Make sure to update both sides of a bidirectional association (or at least the owning side, from Doctrine’s point of view)
This is my problem. My changes to my inverse side are ignored. How can I fix this ? I tried all those things mentioned by other users, but still doesn't work. Thanks in advance.
UPDATE 1
Here is my method for editing Students. Please note that if I switch the inverse and owning side (in the YAML file configuration), the Study Groups editing will work, and the Students editing will not work. (by not work, I mean it does not create the association between the two)
public function editStudentAction(Request $request, $student) {
/* DATABASE SERVICE */
$this->db_helper = $this->get('db.helper');
/* DOCTRINE */
$em = $this->getDoctrine()->getManager();
$student = $em->getRepository('AppBundle:Students')->find($student);
/* INVALID ID */
if (count($student) == 0) {
/* INVALID ID */
$request->getSession()->getFlashBag()->add(
'error', 'Invalid ID!'
);
return $this->redirect($this->generateUrl('index'));
}
/* GET STUDENT FORM */
$form = $this->newStudentForm($student, "edit");
/* HANDLE FORM SUBMISSION */
$form->handleRequest($request);
if ($form->isValid()) {
/* CHECK IF STUDENT ONLY WANT TO ENROLL 2 STUDY GROUPS */
if (count($student->getStudyGroups()) > 2) {
$request->getSession()->getFlashBag()->add(
'error', 'Students may only enroll 2 study groups at the same time!'
);
} else {
try {
/* SAVE TO DATABASE */
$em->persist($student);
$em->flush();
/* REDIRECT TO MAIN PAGE */
return $this->redirect($this->generateUrl('index'));
} catch (\Doctrine\DBAL\DBALException $e) {
/* HANDLE DUPLICATE KEY ISSUE */
if ($e->getPrevious()->getCode() === '23000') {
$request->getSession()->getFlashBag()->add(
'error', 'User already exists!'
);
} else
throw $e;
}
}
}
return $this->render('default/new.html.twig', array(
'form' => $form->createView(),
'object' => "student"
));
}
I'm highly surprised this works either side, as it would appear you have a potential infinite loop happening.
addStudent() calls addStudyGroup() which calls addStudent() which calls addStudyGroup() which calls addStudent()............ Remove those two "ADDED THIS LINE", this needs to happen in your controller.
Here's an update to go in your controller. First you need to remember the existing list of study groups for this student before you create the form:
/* GET ORIGNAL COLLECTIONS */
$originalGroups = new ArrayCollection(); // requires: use Doctrine\Common\Collections\ArrayCollection;
foreach ($student->getStudyGroups() as $group) {
$originalGroups->add($group);
}
/* GET STUDENT FORM */
Now you need to remove any removed study groups from the new collection,
/* MANY TO MANY */
foreach ($originalGroups as $group) {
if (!$student->getStudyGroups()->contains($group)) {
$student->removeStudyGroup($group);
$group->removeStudent($student);
$em->remove($group);
} else {
$group->addStudent($student);
$em->persist($group);
}
}
/* SAVE TO DATABASE */
$em->persist($student);
Related
I have some entities and when I find an interaction (CRUD) for each of them I stock the action in a Logs entity.
My Logs entity has FK to some other entities, and these FK fields can be null.
When I create my new Logs it says:
Expected type 'App\Entity\Client'. Found 'null'.
For the precision, everything works perfectly without the Logs part so I need to fix it to have the result I want.
My Logs entity on where I get the error:
#[ORM\ManyToOne(inversedBy: 'logs', cascade: ['persist'])]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Client $client = null;
My Client entity:
#[ORM\OneToMany(mappedBy: 'client', targetEntity: Logs::class, orphanRemoval:false, cascade: ['persist'])]
private Collection $logs;
PS: I want the logs to not be deleted in cascade while deleting the stuff it refers to.
EDIT:
In my Group entity:
public function getClient(): ?Client
{
return $this->client;
}
In my Client entity:
/**
* #return Collection<int, Group>
*/
public function getGroups(): Collection
{
return $this->groups;
}
Here is where I get the error:
#[Route('/new', name: 'app_user_new', methods: ['GET', 'POST'])]
public function new(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserRepository $userRepository, LogsRepository $logsRepository, GroupRepository $groupRepository, SiteRepository $siteRepository, ClientRepository $clientRepository): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$userPasswordHasher->hashPassword(
$user,
$form->get('password')->getData()
)
);
$userRepository->save($user, true);
// take all needed information
$now = new DateTime('now');
$action = 'Create "'.$user->getMail().'" user';
$result = 'SUCCESS';
$gw_interaction = 'none';
$client = $clientRepository->clientToCreateAndDelete($clientRepository);
$group = $groupRepository->groupToCreateAndDelete($client, $groupRepository);
$site = $siteRepository->siteToCreateAndDelete($group, $siteRepository);
// write this action on the logs
$logsRepository->createANewLog($now, $action, $result, $gw_interaction, $user, $client, $site);
// delete the factice client
// $groupRepository->remove($group, true);
$clientRepository->remove($client, true);
return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('user/new.html.twig', [
'user' => $user,
'form' => $form,
]);
}
I can't put null as a value so I tried to create the needed entities, add the log and then remove the entities but it also doesn't work. It says I can't delete a parent row before children's (I have a group a child of a client) But if I delete the group and then delete the client, I still have this error. This is why I just want to be able to give a null at the beginning so I do not need to create and delete useless entities.
My function looks like this:
public function createANewLog(DateTime $now, String $action, String $result, String $gw_interaction, User $user)
{
$logs = new Logs();
$logs->setDatetime($now);
$logs->setAction($action);
$logs->setResultat($result);
$logs->setGwInteraction($gw_interaction);
$logs->setHisUser($user);
$this->save($logs, true);
}
One possible solution:
Because I cannot solve this I found another way to save my values. Now, I deleted client and site columns in Logs:
-> when I need them to be null I just do nothing.
-> when I need them to have a value, I wrote the values in the action:
-> before action had for example: "Create site siteName"
-> now action is for example: "Create site siteName from group
GroupName"
I have question concerning easyadmin3. In my admin panel I have a productCrudController and one of the values I want to be able to set when creating a new product is the price.
For the price I have a separate table though which contains all my prices with a date. The idea being that the price of a product van change over time and my client wants to be able to have an overview of the price history for each product.
So in my productCrudController I work with an associationField to link to my prices entity. However I'm really stuck with the following practical issue: I don't want to have to add a price in a priceCrudController which I would then be able to select in my productCrudController (the way the associationField expects me to do).
What I want is that I can create a product and input a price which would then be inserted into my prices table.
My code:
productCrudController ->
Right now I have a field for prices where I can select a price in a dropdown menu, but so I have to add the price first with a priceCrudController, which really isn't practical.
class ProductsCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Products::class;
}
public function configureFields(string $pageName): iterable
{
$image = ImageField::new('image')->setBasePath('resources/images');
$imageFile = TextField::new('imageFile')->setFormType(VichImageType::class);
$fields = [
IdField::new('id', 'ID')->hideOnForm(),
TextField::new('name'),
TextEditorField::new('description'),
AssociationField::new('category'),
AssociationField::new('plants')->setTemplatePath('list.html.twig'),
NumberField::new('stock'),
AssociationField::new('prices', 'bruto price')->onlyOnIndex()->setTemplatePath('price.html.twig'),
];
if($pageName == Crud::PAGE_INDEX || $pageName == Crud::PAGE_DETAIL){
$fields[] = $image;
} else {
$fields[] = $imageFile;
}
return $fields;
}
I tried just making a numberField for 'prices' to see if I could just enter a value that would then be persisted in the database, but I get the following error:
Object of class Doctrine\ORM\PersistentCollection could not be
converted to string
This is my 'prices' property in my 'products' entity and the methods:
/**
* #ORM\OneToMany(targetEntity=Prices::class, mappedBy="product")
* #Groups({"products:read"})
*/
private $prices;
/**
* #return Collection|Prices[]
*/
public function getPrices(): Collection
{
return $this->prices;
}
public function addPrice(Prices $price): self
{
if (!$this->prices->contains($price)) {
$this->prices[] = $price;
$price->setProduct($this);
}
return $this;
}
public function removePrice(Prices $price): self
{
if ($this->prices->removeElement($price)) {
// set the owning side to null (unless already changed)
if ($price->getProduct() === $this) {
$price->setProduct(null);
}
}
return $this;
}
I have the feeling I might need to do something with event listeners, but I don't really know how to go about it as I haven't really worked with them before.
I'd be very grateful for any help
You can create a form for the Prices entity and then use it in your product
CollectionField::new('prices')
->hideOnIndex()
->setLabel('bruto price')
->setTemplatePath('price.html.twig')
->setFormTypeOptions([
'label' => false,
'delete_empty' => true,
'by_reference' => false,
])
->setEntryIsComplex(false)
->setCustomOptions([
'allowAdd' => true,
'allowDelete' => false,
'entryType' => PricesType::class, // Your price form class here
'showEntryLabel' => false,
])
;
i have a crazy problem, which i dont understand.
My code where is concerned looks like that:
public function appendAction(Request $request, $pKpPatientid)
{
if (!$this->isAdmin()) {
throw new AccessDeniedException();
}
$entity = new DataDFu1();
$entity1 = $this->getDoctrine()
->getRepository('DataLiveBundle:DataAPatient')
->find($pKpPatientid);
$appendForm = $this->createAppendForm($pKpPatientid,$entity, $entity1);
$appendForm->handleRequest($request);
// if ($appendForm->isValid()) {
if($appendForm->get('submit')->isClicked()){//Save
//return $this->redirect($this->generateUrl('dataapatient_sendMessage', array("pKpPatientid" => $pKpPatientid)));
$entity->setFu1KfPatientid($entity1);
$this->storeAppendDataDFu1($entity);
// }
}
return $this->render('DataLiveBundle:DataDFu1:form.html.twig', array(
// 'entity' => $entity,
'form' => $appendForm->createView(),
'isNew'=> true,
));
}
/**
* The function createAppendForm
* Creates a form with the Information from a DataAPatient.
* #param DataAPatient $pKpPatientid The primary key
* #return \Symfony\Component\Form\Form
*/
private function createAppendForm($pKpPatientid, $entity, $entity1)
{
$form = $this->createForm($this->get('data_livebundle.form.dataapatienttype'), $entity1, array(
//'action' => $this->generateUrl('dataHome'),
'method' => 'POST'
));
$form->add('submit', 'submit', array('label' => 'Create Fu1'));
return $form->add('dFu1', new DataDFu1Type(), array('data'=>$entity));
}
/**
* The function storeEditedDataDFu1
* Persists changes made to an existing DataDFu1 entity to the database
* #param DataDFu1 entity
* #return DataAPatient $pKpPatientid The primary key
*/
public function storeAppendDataDFu1($entity)
{
$em = $this->getDoctrine()->getManager();
$session = $this->getRequest()->getSession();
if (!$entity) {
throw $this->createNotFoundException('Unable to find DataDFu1 entity.');
}
$em->persist($entity);
$em->flush();
$session->getFlashBag()->add(
'notice',
'Your changes to the DataDFu1 of ID: "'."xyz". '" was saved!'
);
// return $entity->getPKpPatientid();
}
I create a form which inludes two forms with entities and render it. It works fines. But in this moment when in want store (storeAppendDataDFu1) the data from the entity and just from this entity the entity1 loses all values that were previously visualized in the form(just the visualized). This means that the fields displayed by this entity1 are stored persistently with the value NULL in the database.
How can entity1 persistently store the wrong values even though only one memory function has been programmed for the entity1 ()???
My presumption says that it has to do with the constellation of form, because whenever I press Submit, the fields of entity1 are set to NULL.
I hope somebody know this problem:), i really can not find a solution.
*the entity and entity1 are just connected together, because the foreig key from entity is entity1's primary key its an oneToOne matching...
i found also the mappingBy null? what means that? could it be the reason?
oneToOne:
fu1KfPatientid:
targetEntity: DataAPatient
cascade: { }
fetch: LAZY
mappedBy: null
inversedBy: dFu1
joinColumns:
_FU1_kf_PatientID:
referencedColumnName: __P_kp_PatientID
orphanRemoval: false
thanks for a feedback... tell me if you need more information..Thaanks
If the entities are related is not better to add the proper mapping information to persist both entities?
Entity 1
/**
*#ORM\OneToOne(targetEntity="Entity2", cascade={"persist"})
*
*/
$entity_two_reference;
in the form this would be a Entity2Type and should render as usual
when you make the flush() it should work fine.
now i found the issue! The Problem is that i'm using
Data-Toggle
{#<div class="container">
<div class="row">
<ul class="nav nav-tabs pull-left">
<li class="active">Patient info</li>
<li >Part I</li>
<li >Part II</li>
<li >Part III</li>
</ul>
{#div class="clearfix">tmp. disable#}</div> #}
to create a Register Layout. I think this kind of Layout doesn't works fine with Simfony2 Forms. Has somebody experiance which kind of Register Layout works with Symfony2 Forms?
On some Company (entity) page I'm displaying several Categories (entity) which the Company might have. For every of the categories displayed I need to add simple form with rating value. So I've created entity Called Rating and I've generated RatingType.
Now I have the problem with display this form for each category displayed, I can display the form only once for the first category occurrence, but it in the Rating Form it can't get the name (the id) of the category which it should be connected.
The Rating entity have defined Category and Company in relation ManyToOne (#ORM).
I would appreciate for help how can I handle with that.
I suppose that the trick sits in the Controller, so below is my code:
/**
* #Route("/catalog/{id}.html", name="card_show")
*/
public function cardShowAction(Company $company, Request $request)
{
$form = null;
// #TODO: add verification if user is logged on => if ($user = $this->getUser())
$rating = new Rating();
$rating->setCompany($company);
$form = $this->createForm(RatingType::class, $rating);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted()) {
$em = $this->getDoctrine()->getManager();
$em->persist($rating);
$em->flush();
$this->addFlash('success', "Your vote has been saved!");
return $this->redirectToRoute('card_show', array('id' => $company->getId()));
}
return $this->render("default/catalog/show.html.twig", array(
'card' => $company,
'form' => is_null($form) ? $form : $form->createView()
));
}
Here is the RatingType code:
class RatingType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('comment')
->add('vote')
// ->add('userName')
// ->add('ip')
// ->add('createdAt')
->add('service')
// ->add('company')
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Rating'
));
}
}
Each Symfony Form can be rendered in view only once (at least without nasty hacks).
I propose another approach. In your template render as many simple html forms (not binded with Symfony Forms at all!), as many categories you have. Then, give each of those form another action attribute (say "/catalog/{company_id}/category/{category_id}/vote"). Then, create new action, where you will parse, validate and use data from Request object.
If you really want to do all of this using Symfony forms you need to generate as many Symfony Forms instances, as many categories you will have. Each form needs to have unique name and action (or some hidden field) to distinct category user would like to vote.
I have problem saving entity trough form with ManyToMany relations.
I can not save fields that are on "mappedBy" side of relation.
Code below is not saving anything to database and not trowing any errors:
// Entity/Pet
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Customer", mappedBy="pet", cascade={"persist"})
*/
private $customer;
/**
* Set customer
*
* #param \AppBundle\Entity\Customer $customer
* #return Pet
*/
public function setCustomer($customer)
{
$this->customer = $customer;
return $this;
}
// Entity/Customer
/**
* #var Pet
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Pet", inversedBy="customer", cascade={"persist"})
* #ORM\JoinTable(name="customer_pet",
* joinColumns={
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="pet_id", referencedColumnName="id")
* }
* )
*/
private $pet;
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
));
It is working the other way around. So if I am saving something from CustomerType everything works.
EDIT:
Solution below worked for me but after couple days I found a problem with that solution. If form will be submitted with value that has been already saved in the database then Symfony will trow an error. To prevent that I had to check if given customer has been already assigned to the pet.
Checking of currently assigned customers had to be done on the beginning of function and not after form submission because for some reason after submission Pet() object contains submitted values not only those present in the db.
So on the beginning I've putted all already assigned customers in to the array
$em = $this->getDoctrine()->getManager();
$pet = $em->getRepository('AppBundle:Pet')->find($id);
$petOriginalOwners = array();
foreach ($pet->getCustomer() as $petCustomer)
{
$petOriginalOwners[] = $petCustomer->getId();
}
And after form submission I've checked if submitted ID's are in the array
if ($form->isValid())
{
foreach ($form['customer']->getData()->getValues() as $v)
{
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer && !in_array($v->getId(), $petOriginalOwners) )
{
$customer->addPet($pet);
}
}
$em->persist($pet);
$em->flush();
return $this->redirect($this->generateUrl('path'));
}
In Symfony2 the entity with the property with the inversedBy doctrine comment is the one that is supposed to EDIT THE EXTRA TABLE CREATED BY THE MANYTOMANY RELATION. That is why when you create a customer it inserts the corresponding rows in that extra table, saving the corresponding pets.
If you want the same behavior to happen the other way around, I recommend:
//PetController.php
public function createAction(Request $request) {
$entity = new Pet();
$form = $this->createCreateForm($entity);
$form->submit($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($form['customer']->getData()->getValues() as $v) {
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer) {
$customer->addPet($entity);
}
}
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('pet_show', array('id' => $entity->getId())));
}
return $this->render('AppBundle:pet:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
private function createCreateForm(Pet $entity) {
$form = $this->createForm(new PetType(), $entity, array(
'action' => $this->generateUrl('pet_create'),
'method' => 'POST',
));
return $form;
}
These two are but standard Symfony2 CRUD-generated actions in the controller corresponding to Pet entity.
The only tweak is the foreach structure inserted in the first action, that way you forcibly add the same pet to each customer you select in the form, thus getting the desired behavior.
Look, it is highly probable THIS is not the RIGHT WAY, or the PROPER WAY, but is A WAY and it works. Hope it helps.
In my case with a services <-> projects scenario, where services has "inversedBy" and projects has "mappedBy" I had to do this in my project controller's edit action so that when editing a project the services you checked would be persisted.
public function editAction(Request $request, Project $project = null)
{
// Check entity exists blurb, and get it from the repository, if you're inputting an entity ID instead of object ...
// << Many-to-many mappedBy hack
$servicesOriginal = new ArrayCollection();
foreach ($project->getServices() as $service) {
$servicesOriginal->add($service);
}
// >> Many-to-many mappedBy hack
$form = $this->createForm(ProjectType::class, $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
// << Many-to-many mappedBy hack
foreach ($servicesOriginal as $service) {
if (!$project->getServices()->contains($service)) {
$service->removeProject($project);
$em->persist($service);
}
}
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
$em->persist($project);
$em->flush();
return; // I have a custom `redirectWithMessage()` here, use what you like ...
}
return $this->render("Your-template", [
$form => $form->createView(),
$project => $project,
]);
}
This works for both adding and removing entities in the many-to-many from the "mappedBy" side, so EntityType inputs should work as intended.
What's going on here is we're first building an "original" collection containing all of the service entities already linked to for this project. Then when the form is saving we're ensuring:
First that any unchecked services (those in the original collection but not the project object) have the project removed from their internal collection, then persisted.
Second that any newly checked services each add the project to their internal collection, then persisted.
Important: This depends on your entity's addService() and addProject() methods respectively check that each others' collections do not contain duplications. If you don't do this you'll end up with an SQL level error about a duplicate record insertion.
In the service entity I have:
/**
* Add project
*
* #param Project $project
*
* #return Service
*/
public function addProject(Project $project)
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
}
if (!$project->getServices()->contains($this)) {
$project->getServices()->add($this);
}
return $this;
}
In the project entity I have:
/**
* Add service
*
* #param Service $service
*
* #return Project
*/
public function addService(Service $service)
{
if (!$this->services->contains($service)) {
$this->services->add($service);
}
if (!$service->getProjects()->contains($this)) {
$service->getProjects()->add($this);
}
return $this;
}
You could alternatively check this in your controller instead, but makes sense if the model validates this itself when possible, as the model would break anyway if there were duplicates from any source.
Finally in your controller's create action you'll likely need this bit too just before $em->persist($project). (You won't need to work with an "original" collection as none will exist yet.)
// << Many-to-many mappedBy hack
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
I just had the same problem and I solved it differently.
Changing the code in the controller is not the better way to do it.
In my case I have a GenericController that handle all my CRUDs so I can't put in it specific code.
The best way to do it is by adding in your PetType a listener like this :
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
))
->addEventListener( FormEvents::SUBMIT, function( FormEvent $event ) {
/** #var Pet $pet */
$pet = $event->getData();
foreach ( $pet->getCustomers() as $customer ) {
$customer->addPet( $pet );
}
} );
That way you'll keep the mapping logic in the same place.