one to many setter without object - symfony

I would like to be able to set a foreign key just by its id.
Sometimes, for some long scripts, the fact that I need to give the full foreign object to my setter method force me to do some database queries, wasting resources.
$entity = new SomeEntity();
$entity->setIdAnswer(42);
$em->persist($entity);
Instead of
$world = $em->getRepositorye('My/Bundle:Answer')->findOneById(42);
$entity = new SomeEntity();
$entity->setIdAnswer( $world );
$em->persist( $entity);
How is it possible to occasionally set the foreign key with its integer key?
It would be great if we can do that without using some dirty code

Usually you can achieve exactly that with reference proxies:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/advanced-configuration.html#reference-proxies
// $em instanceof EntityManager, $cart instanceof MyProject\Model\Cart
// $itemId comes from somewhere, probably a request parameter
$item = $em->getReference('MyProject\Model\Item', $itemId);
$cart->addItem($item);

Related

Symfony update entity without changing field

Is there a way to update the entity without actually changing any fields.
$em = $this->getDoctrine()->getManager();
$test = $em->getRepository('AppBundle:MyObject')->find($id);
$test->setName("Test");
$em->flush();
Works for example if the name was set to something else like "Test2" before. But if it was "Test" before this won't work. So I would like to remove the setName line and just update entity.
I need this cause I have a listener that will only run when MyObject changes.
I tried $em->refresh($test); but it doesn't work.
well, you want to update an object but anything has changed ... I recommend rethinking the implementation of your listener or you can do this (very ugly) workaround
$em = $this->getDoctrine()->getManager();
$test = $em->getRepository('AppBundle:MyObject')->find($id);
$originalName = $test->getName();
$test->setName("Whatever");
$em->flush();
$test->setName($originalName)
$em->flush();
I would not recommend this solution, you should really implement your listener in the way it trigger every time you needed to.
The events in a listener like you're describing work on the Doctrine UnitOfWork --meaning that they fire and then look at the differences between the previous state of the object and the intended state of the object. I can't think of a way to get Doctrine to persist something that hasn't changed.
In this case, I don't think you want a listener. I think you want a service that you call explicitly.
$em = $this->getDoctrine()->getManager();
$test = $em->getRepository('AppBundle:MyObject')->find($id);
$testService = $this->get('test_service');
$testService->whatMethodYouWantToRunRegardlessOfChanges($test);
$em->flush();
And then in TestService:
public function whatMethodYouWantToRunRegardlessOfChanges(Test $test) {
//code that you were running in your listener, called every time
}

Update data without form using Symfony2

I've tried searching about updating data in Symfony2 but look like all tutorials need few normal steps to do this :
Manager initialisation $em = $this->getDoctrine()->getManager();
Make Entity with criteria $entity =
$em->getRepository('bundle')->find($id);
Create Form $form = $this->createForm(new Type(), $entity);
Bind with request $editForm->handleRequest($request);
Flush data $em->flush();
Let say that I have custom form in twig and do manual getRequest in controller $variable = $request->request->get('name');. Is there any way I can do to update this data for specific ID in entity $entity = $em->getRepository('bundle')->find($id); without create a form for flush my data?
Because I need to update this variable for many ID in my database using iteration. Let say that I have thousands data need to updated with this value. I'm worried if creating form will impact to performance and time.
Simply set your data directly in your entity using your setters and then flush:
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('Bundle:Entity')->find($id);
$entity->setSomeProperty($propertyValue);
$em->flush();

Entity persist finds new relationship that existed before

Hi I have an action which adds people to a group.
In order to increase the usability the form for it removes the people that are already in the group.
My controller action looks like this:
public function addAction(UserGroup $userGroup)
{
$tempGroup = new UserGroup();
foreach ($userGroup->getUsers() as $user) {
$tempGroup->addUser($user);
}
$form = $this->container->get('form.factory')->create(new UserGroupQuickType(), $tempGroup);
$request = $this->container->get('request');
if ('POST' === $request->getMethod()) {
$form->submit($request);
if ($form->isValid()) {
$group = $form->getData();
/** #var $myUserManager UserManager */
$myUserManager = $this->container->get('strego_user.user_manager');
/** #var $em EntityManager */
$em = $this->container->get('em');
foreach ($group->getUsers() as $toInvite) {
$userGroup->addUser($toInvite);
}
$em->persist($userGroup);
$em->flush($userGroup);
}
}
return array(
'form' => $form->createView(),
'userGroup' => $userGroup
);
}
This code throws an exception:
A new entity was found through the relationship 'Strego\UserBundle\Entity\UserGroup#users'
that was not configured to cascade persist operations for entity: Degi.
To solve this issue: Either explicitly call EntityManager#persist() on
this unknown entity or configure cascade persist this
association in the mapping for example #ManyToOne(..,cascade={"persist"}).
The new found relation with was already there before. Meaning the user "Degi" was already in the group and is not a new entity.
I can avoid this error if I'll leave out the persist but then I'll get an exception: Entity has to be managed or scheduled for removal for single computation.
This is caused by the fact that my "usergroup" entity has the whole time a entity status of 3 (= detached)
I have used the same logic (with temp group etc.) for an entity that has 1 to 1 relationship to my usergroup and from there I can easily add people even to the group.
But not with this action, which is logically doing the exact same thing.
UPDATE:
My previous update was leading in the wrong direction. But here in comparison the (almost) same controller that works:
public function addAction(BetRound $betRound)
{
$userGroup = new UserGroup();
foreach ($betRound->getUsers() as $user) {
$userGroup->addUser($user);
}
$form = $this->createForm(new UserGroupQuickType(), $userGroup);
$request = $this->getRequest();
if ('POST' === $request->getMethod()) {
$form->submit($request);
if ($form->isValid()) {
/** #var $betRoundManager BetRoundManager */
$betRoundManager = $this->container->get('strego_tipp.betround_manager');
/** #var $myUserManager UserManager */
$myUserManager = $this->container->get('strego_user.user_manager');
$group = $form->getData();
foreach ($group->getUsers() as $toInvite) {
if (!$betRound->getUserGroup()->hasUser($toInvite)) {
$betRound->getUserGroup()->addUser($toInvite);
}
}
$this->getDoctrine()->getManager()->flush();
return $this->redirect($this->generateUrl('betround_show', array('id' => $betRound->getId())));
}
}
return array(
'form' => $form->createView(),
'betRound' => $betRound
);
}
This is expected behavior since your have relationship to, not really a new entity but unmanaged object.
While iterating you could try merging $toInvite. Don't worry, if objects you acquired via form have an appropriate identifier (#Id) value set they would be just reloaded from databases instead of marked for insertion. Newly added objects, on the other hand, will be marked.
So, before trying anything, ensure that each of old $toInvite have an ID set.
foreach ($group->getUsers() as $toInvite) {
$em->merge($toInvite);
$userGroup->addUser($toInvite);
}
// safe to do now, all of the added users are either reloaded or marked for insertion
$em->perist($usetGroup);
$em->flush($userGroup);
Hope this helps.
This is happening because $userGroup is already an entity which already exists and $toInvite is a new entity and you are trying to flush $userGroup. To make this code working you need to specify cascade={"persist"} in your entity file (Annotations or yaml whichever you prefer).
Other solution is to do the reverse and persist $toInvite
foreach ($group->getUsers() as $toInvite) {
$toInvite->setUserGroup($userGroup);
$em->persist($toInvite);
}
$em->flush();
I think the first option would be a better choice
After several attempts I found out that this issue were actually 2 issues:
1. I had a listener that updated some rows on persist even if it was not necessary.
2. The paramconverter somehow detaches the entities that are coming in. Which means that my UserGroup and all my users in it are detached. As soon as they are added again to the Usergroup, those Entities seem to be "new" for the EM. Therefore I needed to merge all users AND fix my listener that updates the "createdBy", which otherwise would have also needed to been merged.

Query only works if I run a query before it

I want to get the schools the user is in, but for some reason I can only access it by running a query to the school table that is otherwise unrelated. Here is my code:
This doesn't work (within the controller):
$schoolsEnrolled = $this->getUser()->getSchools();
The result is an Array with a School object with all it's properties as null (other than id for some reason).
This does work (within the controller):
//unrelated query
$repository = $this->getDoctrine()->getRepository('AcmeMainBundle:School');
$query = $repository->createQueryBuilder('s')->getQuery();
$schools = $query->getResult();
//the query I care about
$schoolsEnrolled = $this->getUser()->getSchools();
The result is an array of schools as desired.
Here are the related methods:
In the School Class:
public function getSchools(){
$schools = array();
foreach ($this->schoolHasUsers as $key=>$schoolHasUser){
$schools[] = $schoolHasUser->getSchool();
}
return $schools;
}
In the SchoolHasUser Class:
public function getSchool()
{
return $this->school;
}
How can I get the query I care about to work without the unrelated query?
In doctrine object can be lazy-loaded. Then you call $schoolHasUser->getSchool(); you trully no query the database, only get proxy object. So try to get some property of it, example getName or getId. This action make a call to DB and fetch object.
One of the proper ways to do it is like this
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('YourBundle:YourEntity')->findAll();

How to update a Doctrine Entity from a serialized JSON?

We are using Symfony2 to create an API. When updating a record, we expect the JSON input to represent a serialized updated entity. The JSON data will not contain some fields (for instance, CreatedAt should be set only once when the entity is created - and never updated). For instance, here is an example JSON PUT request:
{"id":"1","name":"anyname","description":"anydescription"}
Here is the PHP code on the Controller that should update the entity according to the JSON above (we are using JMS serializer Bundle):
$supplier = $serializer->deserialize(
$this->get('request')->getContent(),
'WhateverEntity',
'json'
);
The EntityManger understands (correctly) that this is an update request (in fact, a SELECT query is implicitly triggered). The EntityManager also guess (not correctly) that CreatedAt property should be NULLified - it should instead keep the previous one.
How to fix this issue?
It's possible as well to do it with Symfony Serializer using object_to_populate option.
Example: I receive JSON request. If record exists in database I want to update fields received in body, if it does not exist I want to create new one.
/**
* #Route("/{id}", methods={"PUT"})
*/
public function upsert(string $id, Request $request, SerializerInterface $serializer)
{
$content = $request->getContent(); // Get json from request
$product = $this->getDoctrine()->getRepository(Product::class)->findOne($id); // Try to find product in database with provided id
if (!$product) { // If product does not exist, create fresh entity
$product = new Product();
}
$product = $serializer->deserialize(
$content,
Product::class,
'json',
['object_to_populate' => $product] // Populate deserialized JSON content into existing/new entity
);
// validation, etc...
$this->getDoctrine()->getManager()->persist($product); // Will produce update/instert statement
$this->getDoctrine()->getManager()->flush($product);
// (...)
using the JMSSerializerBundle follow the install instructions at
http://jmsyst.com/bundles/JMSSerializerBundle
either create your own serializer service or alter the JMSSerializerBundle to use the doctrine object constructor instead of the simple object constructor.
<service id="jms_serializer.object_constructor" alias="jms_serializer.doctrine_object_constructor" public="false"/>
This basically handles exactly what Ocramius solution does but using the JMSSerializerBundles deserialize.
I would use the Doctrine\ORM\Mapping\ClassMetadata API to discover existing fields in your entity.
You can do following (I don't know how JMSSerializerBundle works):
//Unserialize data into $data
$metadata = $em->getMetadataFactory()->getMetadataFor($FQCN);
$id = array();
foreach ($metadata->getIdentifierFieldNames() as $identifier) {
if (!isset($data[$identifier])) {
throw new InvalidArgumentException('Missing identifier');
}
$id[$identifier] = $data[$identifier];
unset($data[$identifier]);
}
$entity = $em->find($metadata->getName(), $id);
foreach ($metadata->getFieldNames() as $field) {
//add necessary checks about field read/write operation feasibility here
if (isset($data[$field])) {
//careful! setters are not being called! Inflection is up to you if you need it!
$metadata->setFieldValue($entity, $field, $data[$field]);
}
}
$em->flush();

Resources