How to use Lock Component in Symfony3.4 with username - symfony

I am trying to use the lock component in symfony 3.4, like it is described on
https://symfony.com/doc/3.4/components/lock.html
I want to prevent multiple data changes from different users.
For example user1 is calling the same company form with data, then user2
How can I tell user2, that editing data is blocked by user1 (incl username) ?
UPDATE:
It is used in backend, where a lot of employees editing data of customers, order etc.
this form is just for editing. that means, if they want to update some data, they click "edit". They should be informed when another employee changes this record before the data is loaded into the form. It sometimes takes some time for the employee to change everything. If the employee receives a message when saving it, they have to go back,reload the data and start all over again.
an example out of my controller:
public function CompanyEdit(Request $request)
{
$error = null;
$company_id = $request->get('id');
// if (!preg_match('/^\d+$/', $company_id)){
// return $this->showError();
// }
$store = new SemaphoreStore();
$factory = new Factory($store);
$lock = $factory->createLock('company-edit-'.$company_id, 30);
if(!$lock->acquire()) {
//send output with username
// this data is locked by user xy
return 0;
}
$company = $this->getDoctrine()->getRepository(Company::class)->find($company_id);
$payment = $this->getDoctrine()->getRepository(Companypay::class)->findOneBy(array('company_id' => $company_id));
$form = $this->createFormBuilder()
->add('company', CompanyFormType::class, array(
'data_class' => 'AppBundle\Entity\Company',
'data' => $company
))
->add('payment', CompanyPayFormType::class, array(
'data_class' => 'AppBundle\Entity\CompanyPay',
'data' => $payment
))
->getForm();
$form->handleRequest($request);
$company = $form->get('company')->getData();
$payment = $form->get('payment')->getData();
if ($form->isSubmitted() && $form->isValid()) {
$event = new FormEvent($form, $request);
if ($payment->getCompanyId() == null) {
$payment->setCompanyId($company->getId());
}
try {
$this->getDoctrine()->getManager()->persist($company);
$this->getDoctrine()->getManager()->persist($payment);
$this->getDoctrine()->getManager()->flush();
$this->container->get('app.logging')->write('Kundendaten geändert', $company->getId());
} catch (PDOException $e) {
$error = $e->getMessage();
}
if (null === $response = $event->getResponse()) {
return $this->render('customer/edit.html.twig', [
'form' => $form->createView(),
'company' => $company,
'error' => $error,
'success' => true
]);
}
$lock->release();
return $response;
}

You can't (Locks can't have any metadata), but you probably don't want this in the first place.
In this case, you create a Lock when a user opens the edit page and release it when a user submits the form. But what if the users opens the page and doesn't submit the form? And why can't a user even view the form?
This looks like a XY-problem. I think you're trying to prevent users to overwrite data without knowing. Instead, you can add a timestamp or hash to the form that changes after changing the entity. For example:
<form type="hidden" name="updatedAt" value="{{ company.updatedAt()|date('U') }}" />
And in your form:
<?php
if ($company->getUpdatedAt()->format('U') !== $form->get('updatedAt')->getData()) {
throw new \LogicException('The entity has been changed after you opened the page');
}
Disclaimer: code is not tested and just as an example how this solution can look like.

Related

Symfony 3: How to submit forms in the collection of forms separately?

I'm wondering if I could submit forms inside a collection separately? I have very long form collection with buttons to save each subform (Basically filling and validating the form at once would be difficult). So clicking the button suppose to only submit corresponding subform, but it submits whole collection.
getDoctrine()->getManager();
$user = $this->getUser();
if(!count($user->getApplicants())) {
$app = new Applicant();
$app->setUser($user);
$user->setApplicants($app);
}
if(!count($user->getAddresses())) {
$address = new Address();
$address->setUser($user);
$user->setAddresses($address);
}
if(!count($user->getCompanies())) {
$company = new Company();
$company->setUser($user);
$user->setCompanies($company);
}
if(!count($user->getDirectors())) {
$director = new Director();
$director->setUser($user);
$user->setDirectors($director);
}
$form = $this->createForm('AppBundle\Form\UserType', $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($form->getClickedButton() && 'submitApplicants' === $form-
>getClickedButton()->getName()) {
$applicant = $form->getData()->getApplicants()[0];
$applicant->setUser($user);
$em->persist($applicant);
$em->flush();
return $this->render('admin/index.html.twig', [
'form' => $form->createView()
]);
}
if ($form->getClickedButton() && 'submitAddresses' === $form-
>getClickedButton()->getName()) {
$address = $form->getData()->getAddresses()[0];
$em->persist($address);
$em->flush($address);
return $this->render('admin/index.html.twig', [
'form' => $form->createView()
]);
}
if ($form->getClickedButton() && 'submitCompanies' === $form-
>getClickedButton()->getName()) {
$company = $form->getData()->getCompanies()[0];
$em->persist($company);
$em->flush($company);
return $this->render('admin/index.html.twig', [
'form' => $form->createView()
]);
}
if ($form->getClickedButton() && 'submitDirectors' === $form-
>getClickedButton()->getName()) {
$director = $form->getData()->getDirectors()[0];
$em->persist($director);
$em->flush($director);
return $this->render('admin/index.html.twig', [
'form' => $form->createView()
]);
}
//$em->flush();
}
return $this->render('admin/index.html.twig', [
'form' => $form->createView()
]);
}
}
My opinion is that you need to submit the subform via Javascript. Add some js code to the submit button to:
Perform an ajax POST request to a controller action
Make some checks to the form data
Return back either an error message or some HTML (whatever you want)
Do something with that message
That way you will be able to submit each form separately.
Also change the type of the submit button to plain button. Otherwise you will trigger a form submit for the whole page.

Silex - get right condionals in finish middleware

I want to create a pdf file out of some route-dependant data
{http://example.com/products/123/?action=update}
$app->finish(function (Request $request, Response $response) {
// Make a pdf file, only if:
// - the route is under /products/
// - the action is update
// - the subsequent ProductType form isSubmitted() and isValid()
// - the 'submit' button on the ProductType form isClicked()
});
As a normal form submission process I have:
public function update(Application $app, Request $request)
{
$form = $app['form.factory']->create(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if (!$form->get('submit')->isClicked()) {
return $app->redirect('somewhere');
}
$product = $form->getData();
$app['em']->persist($product);
$app['em']->flush();
return $app->redirect('product_page');
}
return $app['twig']->render('products/update.html.twig', array(
'form' => $form->createView(),
));
}
Questions:
Should I duplicate all of the conditionals in finish middleware?
How to access the Product entity in finish middleware?
Consider having multiple resource types like Products, Services, Users, ...

How to edit sonata user with avatar outside admin bundle

Almost same ISSUES: link, link
UPDATE INFO: - $user = $this->getUser(); set the old image while edit(error form submit). The image replaced with the submitted one value(only value not displaying). WHILE ERROR FORM SUBMIT - I NEED TO DISPLAY THE OLD MEDIA.
NO RELATION WITH SONATA ADMIN.
I have Admin and User role. Both have seperate admin area. User admin area has more complex structure.
I added an Image(avatar) to SonataUser , it works good at admin. Its OneToOne - User and Media.
To edit profile at User Dashboard( Its not SonataAdmin - its i created seperately, Its a simple symfony style).
code:
public function editProfileAction() {
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw $this->createAccessDeniedException('This user does not have access to this section.');
}
// Check user has allready media?
$om = $this->getUser()->getImage();
$oldPath = $om ? $this->getMediaPath($om->getId()) : NULL;
$form = $this->creteForm();
$formHandler = $this->get('sonata.user.profile.form.handler');
$process = $formHandler->process($user);
if ($process) {
// if new file - delete old file
$this->deleteOldMedia($om, $oldPath);
$this->flashMSG(0, 'Profile updated!');
return $this->redirectToRoute('fz_user');
}
$x = ['cmf' => '', 'pTitle' => 'Profile'];
return $this->render(self::TEMPLATE, ['x' => $x, 'form' => $form->createView()]);
}
By the above code, works - with one problem. The reference image of old file is not deleting at server folder. New files are added and entity works fine (displaying at template - fine).
So I tried with my own code,
public function editProfileAction() {
$request = $this->get('request');
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw $this->createAccessDeniedException('This user does not have access to this section.');
}
// Check user has allready media?
$om = $this->getUser()->getImage();
$oldPath = $om ? $this->getMediaPath($om->getId(), 'reference') : NULL;
$oldTN = $om ? $this->getMediaPath($om->getId(), 'admin') : NULL;
$form = $this->createForm(ProfileType::class, $user);
$form->handleRequest($request);
$em = $this->getDoctrine()->getEntityManager();
$data = $form->getData();
if ($form->isSubmitted() && $form->isValid()) {
if (($oldPath != NULL) && ($data->getImage()->getBinaryContent() != NULL)) {
$this->deleteFile($oldPath);
$this->deleteFile($oldTN);
}
$em->persist($user);
$em->flush();
$this->flashMSG(0, 'Profile updated!');
return $this->redirectToRoute('fz_user');
}
// $$user->setImage($om);
$x = ['cmf' => '', 'pTitle' => 'Profile'];
return $this->render(self::TEMPLATE, ['x' => $x, 'form' => $form->createView()]);
}
My own code works - with one problem, If image validation is error - the all image at the template are disappeared. So to check i added $user->setImage(NULL); , the result is, the Null image is shown.(NULL image means at template i do if(null){ display my image }). The backend process - image upadate works good.
For now - I'm satisfied with my code. Here I need to make $user->setImage(xx); to the real image. while form submit with error on media. ONly at error on media.
If no media and error submit - works (displaying image).
UPDATE:
I used $em->refresh($user); from this answer , also it failed to update my image.
WHAT I FOUND ISSUE WITH USER: Its not using 'ApplicationSonataUserBundle:User' for SYMFONY app.user . Thats why, when i give $em->refresh($user); it not modifing username and other details. But it modifing the new details of ApplicationSonataUserBundle:User
Finally to solve I REDIRECTED with flash msg..
$em = $this->getDoctrine()->getManager();
$user = $this->get('security.token_storage')->getToken()->getUser();
$entity = $em->getRepository('ApplicationSonataUserBundle:User')->find($user->getId());
if (!$entity) {
throw $this->createNotFoundException('Unable to find User entity.');
}
$form = $this->createForm(ProfileType::class, $entity);
if ($request->getMethod() === 'POST') {
$form->handleRequest($request);
if ($form->isValid()) {
$em->flush();
return $this->redirectToRoute('fz_user');
}
$em->refresh($user);
$this->flashMSG(1, '' . $form->getErrors(true, false));
return $this->redirectToRoute('fz_user_profile_edit');
}

Prevent duplicates in the database in a many-to-many relationship

I'm working on a back office of a restaurant's website. When I add a dish, I can add ingredients in two ways.
In my form template, I manually added a text input field. I applied on this field the autocomplete method of jQuery UI that allows:
Select existing ingredients (previously added)
Add new ingredients
However, when I submit the form, each ingredients are inserted in the database (normal behaviour you will tell me ).
For the ingredients that do not exist it is good, but I don't want to insert again the ingredients already inserted.
Then I thought about Doctrine events, like prePersist(). But I don't see how to proceed. I would like to know if you have any idea of ​​the way to do it.
Here is the buildForm method of my DishType:
<?php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('category', 'entity', array('class' => 'PrototypeAdminBundle:DishCategory',
'property' => 'name',
'multiple' => false ))
->add('title', 'text')
->add('description', 'textarea')
->add('price', 'text')
->add('ingredients', 'collection', array('type' => new IngredientType(),
'allow_add' => true,
'allow_delete' => true,
))
->add('image', new ImageType(), array( 'label' => false ) );
}
and the method in my controller where I handle the form :
<?php
public function addDishAction()
{
$dish = new Dish();
$form = $this->createForm(new DishType, $dish);
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($dish);
$em->flush();
return $this->redirect($this->generateUrl('prototype_admin_get_dish', array('slug' => $dish->getSlug())));
}
}
return $this->render('PrototypeAdminBundle:Admin:addDish.html.twig', array(
'form' => $form->createView(),
));
}
I was having the same problem. My entities were projects (dishes in your case) and tags (ingredients).
I solved it by adding an event listener, as explained here.
services:
my.doctrine.listener:
class: Acme\AdminBundle\EventListener\UniqueIngredient
tags:
- { name: doctrine.event_listener, event: preUpdate }
- { name: doctrine.event_listener, event: prePersist }
The listener triggers both prePersist (for newly added dishes) and preUpdate for updates on existing dishes.
The code checks if the ingredient already exists. If the ingredient exists it is used and the new entry is discarded.
The code follows:
<?php
namespace Acme\AdminBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\AdminBundle\Entity\Dish;
use Acme\AdminBundle\Entity\Ingredient;
class UniqueIngredient
{
/**
* This will be called on newly created entities
*/
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// we're interested in Dishes only
if ($entity instanceof Dish) {
$entityManager = $args->getEntityManager();
$ingredients = $entity->getIngredients();
foreach($ingredients as $key => $ingredient){
// let's check for existance of this ingredient
$results = $entityManager->getRepository('Acme\AdminBundle\Entity\Ingredient')->findBy(array('name' => $ingredient->getName()), array('id' => 'ASC') );
// if ingredient exists use the existing ingredient
if (count($results) > 0){
$ingredients[$key] = $results[0];
}
}
}
}
/**
* Called on updates of existent entities
*
* New ingredients were already created and persisted (although not flushed)
* so we decide now wether to add them to Dishes or delete the duplicated ones
*/
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// we're interested in Dishes only
if ($entity instanceof Dish) {
$entityManager = $args->getEntityManager();
$ingredients = $entity->getIngredients();
foreach($ingredients as $ingredient){
// let's check for existance of this ingredient
// find by name and sort by id keep the older ingredient first
$results = $entityManager->getRepository('Acme\AdminBundle\Entity\Ingredient')->findBy(array('name' => $ingredient->getName()), array('id' => 'ASC') );
// if ingredient exists at least two rows will be returned
// keep the first and discard the second
if (count($results) > 1){
$knownIngredient = $results[0];
$entity->addIngredient($knownIngredient);
// remove the duplicated ingredient
$duplicatedIngredient = $results[1];
$entityManager->remove($duplicatedIngredient);
}else{
// ingredient doesn't exist yet, add relation
$entity->addIngredient($ingredient);
}
}
}
}
}
NOTE: This seems to be working but I am not a Symfony / Doctrine expert so test your code carefully
Hope this helps!
pcruz
Since this post is 2 years old I don't know if help is still needed here, anyway..
You should place an addIngredient function within your Dish entity, this function checks if the Ingredient object exists within the current ingredient collection.
addIngredient(Ingredient $ingredient){
if (!$this->ingredients->contains($ingredient)) {
$this->ingredients[] = $ingredient;
}
}
Hopefully it could still help you out.

How to remove the null value field from form in symfony form handler

I want to update the data in two conditions:
When user enters all the fields in form (Name, email, password)
When user does not enter password (I have to update only name & email).
I have the Following formHandler Method.
public function process(UserInterface $user)
{
$this->form->setData($user);
if ('POST' === $this->request->getMethod()) {
$password = trim($this->request->get('fos_user_profile_form')['password']) ;
// Checked where password is empty
// But when I remove the password field, it doesn't update anything.
if(empty($password))
{
$this->form->remove('password');
}
$this->form->bind($this->request);
if ($this->form->isValid()) {
$this->onSuccess($user);
return true;
}
// Reloads the user to reset its username. This is needed when the
// username or password have been changed to avoid issues with the
// security layer.
$this->userManager->reloadUser($user);
}
An easy solution to your problem is to disable the mapping of the password field and copy its value to your model manually unless it is empty. Sample code:
$form = $this->createFormBuilder()
->add('name', 'text')
->add('email', 'repeated', array('type' => 'email'))
->add('password', 'repeated', array('type' => 'password', 'mapped' => false))
// ...
->getForm();
// Symfony 2.3+
$form->handleRequest($request);
// Symfony < 2.3
if ('POST' === $request->getMethod()) {
$form->bind($request);
}
// all versions
if ($form->isValid()) {
$user = $form->getData();
if (null !== $form->get('password')->getData()) {
$user->setPassword($form->get('password')->getData());
}
// persist $user
}
You can also add this logic to your form type if you prefer to keep your controllers clean:
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormInterface $form) {
$form = $event->getForm();
$user = $form->getData();
if (null !== $form->get('password')->getData()) {
$user->setPassword($form->get('password')->getData());
}
});
Easier way:
/my/Entity/User
public function setPassword($password)
{
if ($password) {
$this->password = $password;
}
}
So, any form using User with password will act as expected :)

Resources