Parse data after getChangeSet action in Symfony, Doctrine - php

I have this EventSubscriber:
class ChangeLogListener implements EventSubscriber
{
private $tokenStorage;
private $str,$str1;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
'onDelete',
);
}
public function postPersist(LifecycleEventArgs $args)
{
if (!$args->getEntity() instanceof ChangeLog)
$this->createLog($args, 'creation');
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->createLog($args, 'update');
}
public function preRemove(LifecycleEventArgs $args)
{
$this->createLog($args, 'remove');
}
public function createLog(LifecycleEventArgs $args, $action)
{
# Entity manager
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $args->getEntity();
# Get user
$user = $this->tokenStorage->getToken()->getUser();
#Get changes
$changes = $uow->getEntityChangeSet($entity);
$cl = new ChangeLog();
$cl->setDate(new \DateTime());
$cl->setUser($user);
$cl->setEntityName(get_class($entity));
$cl->setEntityId($entity->getId());
$cl->setAction($action);
$cl->setDescription($log);
$cl->setChangeset($changes);
$em->persist($cl);
$em->flush();
}
}
And when I want to POST item, some data must be recorded to db. After all actions I receive this in change_set in my db:
a:3:{s:5:"value";a:2:{i:0;N;i:1;s:3:"120";}s:4:"item";a:2:{i:0;N;i:1;O:21:"AppBundle\Entity\Item":6:{s:25:"AppBundle\Entity\Itemid";i:127;s:27:"AppBundle\Entity\Itemname";s:7:"newitem";s:13:"*categories";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:2:{i:0;O:25:"AppBundle\Entity\Category":7:{s:29:"AppBundle\Entity\Categoryid";i:2;s:31:"AppBundle\Entity\Categoryname";s:10:"child
to
1";s:33:"AppBundle\Entity\Categoryparent";O:40:"Proxies__CG__\AppBundle\Entity\Category":8:{s:17:"isInitialized";b:0;s:29:"AppBundle\Entity\Categoryid";i:1;s:31:"AppBundle\Entity\Categoryname";N;s:33:"AppBundle\Entity\Categoryparent";N;s:35:"AppBundle\Entity\Categorychildren";N;s:8:"*items";N;s:36:"AppBundle\Entity\CategorycreatedAt";N;s:36:"AppBundle\Entity\CategoryupdatedAt";N;}s:35:"AppBundle\Entity\Categorychildren";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:8:"*items";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:36:"AppBundle\Entity\CategorycreatedAt";N;s:36:"AppBundle\Entity\CategoryupdatedAt";N;}i:1;O:25:"AppBundle\Entity\Category":7:{s:29:"AppBundle\Entity\Categoryid";i:4;s:31:"AppBundle\Entity\Categoryname";s:8:"child1.1";s:33:"AppBundle\Entity\Categoryparent";r:13;s:35:"AppBundle\Entity\Categorychildren";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:8:"*items";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:36:"AppBundle\Entity\CategorycreatedAt";N;s:36:"AppBundle\Entity\CategoryupdatedAt";N;}}}s:14:"*initialized";b:1;}s:13:"*attributes";N;s:32:"AppBundle\Entity\ItemcreatedAt";O:8:"DateTime":3:{s:4:"date";s:26:"2018-03-19
10:22:47.000000";s:13:"timezone_type";i:3;s:8:"timezone";s:3:"UTC";}s:32:"AppBundle\Entity\ItemupdatedAt";N;}}s:9:"attribute";a:2:{i:0;N;i:1;O:26:"AppBundle\Entity\Attribute":3:{s:30:"AppBundle\Entity\Attributeid";i:96;s:33:"AppBundle\Entity\Attributealias";s:5:"price";s:32:"AppBundle\Entity\Attributename";s:5:"price";}}}
But I think this data is not readable.I think I need to parse received data before writing it into db, but I don't understand how to parse this into readable format, something like this:
name: Old Value: 12 => New Value: 121, updatedAt: Old Value:
2018-03-20 05:51:44 => New Value: 2018-03-20 08:36:12 and other
Any idea how to parse this?

You are directly inserting all work done on entities with whole object, that's why you are saving all the meta-data into db. Better to doctrine customized extension to handle this (doctrine-extensions and see Loggable behavioral extension for Doctrine2) or if you want to create self customized ChangeLogListner then use methods to compute or get exact change-Set using doctrine methods. to methods see here.
you can change your EventListner code something like this:
$em = $this->getDoctrine()->getManager();
$entity = $em->find('My\Entity', 1);
$entity->setTitle('Changed Title!');
$uow = $em->getUnitOfWork();
$uow->computeChangeSets(); // do not compute changes if inside a listener
$changeset = $uow->getEntityChangeSet($entity);
or check Is there a built-in way to get all of the changed/updated fields in a Doctrine 2 entity
if you are trying inside EventListner then try inside particular events like:
public function preUpdate(Event\LifecycleEventArgs $eventArgs)
{
$changeArray = $eventArgs->getEntityChangeSet();
//do stuff with the change array
}

Related

Update Entity with a new field in a controller - Symfony 6

I'm new in Symfony, and I am trying to calculate the average of the customer reviews at the controller level.
I did a dump in the foreach shown below, where I have the entity as I want, but in the return of the postman the field I added does not exist in my object.
nb: I don't have this field in my user table
My controller:
public function __invoke(UserRepository $rep, Request $request , EntityManagerInterface $em)
{
$user = $this->get('security.token_storage')->getToken()->getUser();
$dataUser = $rep->findUsersData();
$Reviews = $rep->findUsersReviews($user->getId());
$countReviews = count($Reviews);
$starsValues = 0;
foreach($Reviews as $review){
//dump($review);
$starsValues += $review['stars'];
}
$reviewsuservalue = $starsValues / $countReviews;
foreach($dataUser as $key => $userForeach){
if($userForeach->getId() == $user->getId()){
$userForeach->setReviewsuservalue($reviewsuservalue);
//dump($dataUser[$key]->getReviewsuservalue());
$em->persist($userForeach);
$em->flush();
//dump($em->flush());
}
}
return $this->json($dataUser);
}
and I add this in my entity:
private $reviewsuservalue;
and this is my getter & setter in the mentioned entity
public function getReviewsuservalue(): ?float
{
return $this->reviewsuservalue;
}
public function setReviewsuservalue(float $reviewsuservalue): self
{
$this->reviewsuservalue = $reviewsuservalue;
return $this;
}

Dynamically changing properties of row in Symfony 4.4 [duplicate]

This question already has answers here:
Symfony2 Form Entity Update
(3 answers)
Closed 2 years ago.
I'm making a REST API with Symfony 4.4. The API largely revolves around putting data into a database, using Doctrine. I have figured out how to add rows to the database, but now I'm stuck on changing data. I know how I can take a row from the database and that, in theory, I can change fields by calling the setter of a property, but right now, I seem to be getting an array instead of the desired entity and, seemingly more difficult, I want to be able to dynamically change the properties of the existing row, so that I don't have to include every field of the object of the row I'm changing and call every setter.
Here is my code:
// PersonController.php
/**
* #IsGranted("ROLE_USER")
* #Rest\Post("/addperson")
* #param Request $request
* #return Response
*/
public function addOrUpdatePerson(Request $request)
{
$data = json_decode($request->getContent(), true);
$em = $this->getDoctrine()->getManager();
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
$form->submit($data);
if (!$form->isSubmitted() || !$form->isValid())
{
return $this->handleView($this->view($form->getErrors()));
}
if (isset($data['id']))
{
// This person exists, change the row
// What to do?
}
// This person is new, insert a new row
$em->persist($person);
$em->flush();
return $this->handleView($this->view(['status' => 'ok'], Response::HTTP_CREATED));
}
// PersonType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', IntegerType::class, ['mapped' => false])
->add('inits')
->add('firstname')
->add('lastname')
->add('email')
->add('dateofbirth', DateTimeType::class, [
'widget' => 'single_text',
// this is actually the default format for single_text
'format' => 'yyyy-MM-dd',
])
// Some other stuff
->add('save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Person::class,
'csrf_protection' => false
));
}
I doubt the Person entity is relevant here, but if it is, please let me know and I'll include it ASAP!
As a response to the suggestion of the other question from Symfony 2; it doesn't seem to fix my problem (entirely). As a result of this question, I have changed my function to this (which doesn't work, but doesn't throw any errors):
public function addOrUpdatePerson(Request $request)
{
$data = json_decode($request->getContent(), true);
$em = $this->getDoctrine()->getManager();
if (isset($data['id'])) {
// This person exists
$existing = $em->getRepository(Person::class)->find(['id' => $data['id']]);
$this->getDoctrine()->getManager()->flush();
$form = $this->createForm(PersonType::class, $existing);
$form->handleRequest($request);
// this doesn't seem to do anything
// $em->persist($existing);
$em->flush();
return $this->handleView($this->view($existing));
}
}
I think I'm still missing some info, like what to do at // perform some action, such as save the object to the database. I also notice a lot has changed since Symfony 2, and as a result it is not obvious to me what I should do.
After '$person = new Person()' juste add :
If (isset($data['id']) && 0 < $data['id']) {
$person=$em->getRepository(Person::class)->find($data['id']);
}
If (!$person) {
Throw new \Exception('Person not found');
}
1.) You don't have to use json_decode directly. You can use the following code instead:
// Person controller
/**
* #Route("/person", name="api.person.add", methods={"POST"})
* #Security("is_granted('ROLE_USER')")
*/
public function addPerson(Request $request)
{
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
...
}
2.) When you're updating entity you need to load it first and skip the $em->persist($entity); part. In this case, we provide the ID of the entity via the path variable (there are various ways to provide it but this one is quite common). NOTE: I've set $id parameter as mixed because it can be integer or string if you're using UUID type of IDs.
// Person controller
/**
* #Route("/person/{id}", name=api.person.patch", methods={"PATCH"})
* #Security("is_granted('ROLE_USER')")
*/
public function patchPerson(Request $request, mixed $id)
{
// Load person
$personRepository = $this->getDoctrine()->getRepository(Person::class);
$person = $personRepository->find($id);
if (!$person) { throw new \Exception('Entity not found'); }
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->flush();
...
}
3.) In general usage, we don't set the ID property via posted data (unless it is required). We rather use generated value instead. When you insert new entity you gen use its ID to address it for modifications. Sample:
<?php
namespace App\Entity;
use Ramsey\Uuid\Uuid;
use Doctrine\ORM\Mapping as ORM;
class Person
{
/**
* #var Uuid
*
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
* #Groups({"public"})
*/
protected $id;
// Other entity properties ...
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
// Setters and getters for other entity properties ...
}
4.) Entity class in FormType (PersonType.php) is very relevant. After form submission and validation you access properties of the entity itself within the controller - not the decoded payload data from the request directly. Symfony's form system will make sure that the input data is valid and matches the requirements and constraints set in the entity model or form type specification.
// Person controller
/**
* #Route("/person", name="api.person.add", methods={"POST"})
* #Security("is_granted('ROLE_USER')")
*/
public function addPerson(Request $request)
{
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
$id = $person->getId();
$firstName = $person->getFirstname();
$lastName = $person->getLastname();
// etc
...
}
5.) If you want to use the same method/endpoint for adding and updating entity you can do something like #lasouze mentioned.
// Person controller
/**
* #Route("/person", name=api.person.add_or_update", methods={"POST", "PATCH"})
* #Security("is_granted('ROLE_USER')")
*/
public function patchPerson(Request $request)
{
$id = $request->request->get('id', null);
if (!$id) {
$person = new Person();
} else {
// Load person
$personRepository = $this->getDoctrine()->getRepository(Person::class);
$person = $personRepository->find($id);
if (!$person) { throw new \Exception('Entity not found'); }
}
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->flush();
...
}
PS: $form->submit($request->request->all()); will not work for file uploads because $request->request->all() does not contain parameters provided by $_FILES. In my case I ended up merging data like $form->submit(array_merge($request->request->all(), $request->files->all())); but this is probably not the best solution. I'll update my answer if I'll figure out anything better.

How to track changes in Eventlistener and write them into DB?

I am trying to monitor what happens in my app. For this I created this class:
class ChangeLogListener implements EventSubscriber
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
'onDelete',
);
}
public function postPersist(LifecycleEventArgs $args)
{
# Avoid to log the logging process
if (!$args->getEntity() instanceof ChangeLog)
$this->createLog($args, 'creation');
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->createLog($args, 'update');
}
public function preRemove(LifecycleEventArgs $args)
{
# Handle the log creation
$this->createLog($args, 'remove');
}
public function createLog(LifecycleEventArgs $args, $action)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $args->getEntity();
$user = $this->tokenStorage->getToken()->getUser();
$changes[] = $uow->getEntityChangeSet($entity);
$cl = new ChangeLog();
$cl->setDate(new \DateTime());
$cl->setUser($user);
$cl->setEntityName(get_class($entity));
$cl->setEntityId($entity->getId());
$cl->setAction($action);
$cl->setChangeset($changes);//<---change this
$cl->setDescription('');
$em->persist($cl);
$em->flush();
}
}
But I have some problems with changes, I don't know how I can correctly write them into the DB and make them readable. Maybe there are some methods to do this correctly?
When I try to flush changes, I always get error:
String data, right truncated: 1406 Data too long for column
'change_set' at row 1"
My ChangeLog.orm.yml:
#...
fields:
changeSet:
type: array
#...
Do a var_dump($changes);die(); after your getEntityChangeSet() line and figure out what you actually want from that data set. It is currently too long for the field you are trying to save it in. It will need to be reduced.

Objects are not persisted by Doctrine

last couple of days I've been busy making a form using Doctrine and MongoDB. Companies should be able to reserve tables, chairs, .. at a certain event by use of this form. The snippet below shows the controller for this form.
The 'ObjectMap' object maps the amount of a certain object to the object itself. The controller creates all the 'ObjectMap' objects, and adds them to the company object. However, the 'ObjectMap' objects are persisted by Doctrine (they show up in the database) but the company object isn't modified at all, there is no database request made by MongoDB. The persist() function seems to have no effect at all.
public function logisticsAction(Company $company)
{
$form = $this->createForm(new LogisticsForm($this->getDoctrine()->getManager()), $company);
if ($this->getRequest()->isMethod('POST')) {
$form->bind($this->getRequest());
if ($form->isValid()) {
$formData = $this->getRequest()->request->get('_company_logistics_edit');
$objects = $this->getDoctrine()->getManager()
->getRepository('Jobfair\AppBundle\Document\Company\Logistics\Object')
->findAll();
foreach($objects as $object) {
$requirement = $formData['objectRequirement_'.$object->getId()];
$map = new ObjectMap($requirement, $object);
$this->getDoctrine()->getManager()->persist($map);
$company->addObjectMap($map);
//print_r($company->getObjectMaps());
}
$this->getDoctrine()->getManager()->persist($company);
$this->getDoctrine()->getManager()->flush();
$this->getRequest()->getSession()->getFlashBag()->add(
'success',
'The information was successfully updated!'
);
return $this->redirect(
$this->generateUrl(
'_company_settings_logistics',
array(
'company' => $company->getId(),
)
)
);
}
}
The Company object is defined here:
class Company{
/**
* #ODM\Id
*/
private $id;
/*
* #ODM\ReferenceMany(targetDocument="Jobfair\AppBundle\Document\Company\Logistics\ObjectMap", cascade={"persist", "remove"})
*/
private $objectMaps;
public function __construct($name = null, $description = null)
{
$this->objectMaps = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function getObjectMaps()
{
return $this->objectMaps;
}
public function getObjectsArray()
{
$objects = array();
foreach($this->objectMaps as $map)
$objects[] = array(
'name' => $map->getObject()->getName(),
'amount' => $map->getRequirement()
);
return $objects;
}
public function addObjectMap(ObjectMapDocument $objectMap)
{
$this->objectMaps[] = $objectMap;
}
public function removeObject(ObjectMapDocument $objectMap)
{
$this->objectMaps->removeElement($objectMap);
}
}

Refactoring code in controller with Symfony2

I have this code in two methods (create and update). Each time I need to update or create a new user I need to encode the user password with the salt.
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($entity);
$password = $encoder->encodePassword($entity->getPassword(), $entity->getSalt());
$entity->setPassword($password);
To avoid code duplication what should I do?
Create a new method in controller getEncondedPassword($entity) : return $encodedPassword
Add this logic to the Form using DI injecting the $encoder as required field
Add this logic to model, and pass the $encoder in the constructor of the entity object.
Thank you!
If your create and edit are fairly simple and pretty much the same, you can combine it to one function which actually generates and validates the form.
Some code:
class ProductController extends Controller
{
/**
* #Route("/create", name="_product_create")
*/
public function createAction()
{
$product = new Product();
return $this->productForm($product, $this->getRequest(), 'create');
}
/**
* #Route("/edit/{product_id}", name="_product_edit_id")
*/
public function editIdAction($product_id)
{
$entity_manager = $this->getDoctrine()->getEntityManager();
$product_repository = $entity_manager->getRepository('VendorBundle:Product');
$product = $product_repository->findOneBy(
array('id' => $product_id)
);
return $this->productForm($product, $this->getRequest(), 'editId');
}
protected function productForm(Product $product, Request $request, $twig_name)
{
$form = $this->createForm(new ProductType(), $product);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// Do whatever we want before persisting/flushing
return $this->redirect($redirect_url);
}
}
$twig_params = array(
);
return $this->render(
'VendorBundle:Product:' . $twig_name . '.html.twig', $twig_params
);
}
}
this will render create.html.twig and editId.html.twig depending on the route.
if $product->getId() === null we are creating a new entity, else we are editing.
I think that the correct option is the model/entity approach.
So, I leave here the my solution:
public function hashPassword($container)
{
$factory = $container->get('security.encoder_factory');
$encoder = $factory->getEncoder($this);
$password = $encoder->encodePassword($this->getPassword(), $this->getSalt());
return $password;
}
In the controller:
//hash user password
$userEntity->setPassword($userEntity->hashPassword($this->container));
Right now I have improved(I at least think...) the answer to this question.
I have created an class that will receive the $encoderFactory form the DI
#services.yml
parameters:
password_encoder.class: Beubi\SignatureBundle\Handler\PasswordEncoder
services:
password_encoder:
class: %password_encoder.class%
arguments: [#security.encoder_factory]
So, I create a class that will be used in Service container:
class PasswordEncoder
{
protected $encoderFactory;
public function __construct(EncoderFactory $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
public function encodePassword($entity){
$encoder = $this->encoderFactory->getEncoder($entity);
return $encoder->encodePassword($entity->getPassword(), $entity->getSalt());
}
}
And then in my controller:
$password = $this->get('password_encoder')->encodePassword($entity);
$entity->setPassword($password);
This way, my User object doesn't have any knowledge of $factoryEncoder or how to encode an password.
I'm expecting more comments on this question...

Categories