Set order for posted collection entities - symfony

I have two entities, the scale and scale elements. These are available in a OneToMany relationship. The scale elements have an attribute called "sorting". In this column, I will save the order.
Also i create a form ScaleType with collection ScaleElements. I can sort the scale elements with jquery sortable and post the sorted elements. After i post the form i will get the scale elements in posted order to save the "sorting" column.
My edit action which handle this issue looks like:
public function editAction($id, Request $request)
{
$em = $this->getDoctrine()->getManager();
$scale = $em->getRepository('AppMyBundle:Scale')->find($id);
if (!$scale) {
$this->get('session')->getFlashBag()->add('danger', $this->get('translator')->trans('not found'));
return $this->redirect($this->generateUrl('scale_list'));
}
// Create an ArrayCollection of the current Scaleitems objects in the database
$originalScaleElements = new ArrayCollection();
foreach ($scale->getElements() as $element) {
$originalScaleElements->add($element);
}
$form = $this->createForm($this->get('form.type.scale'), $scale);
$form->handleRequest($request);
if ($form->isValid()) {
// remove the relationship between the scale and the elements
foreach ($originalScaleElements as $element) {
if (false === $scale->getElements()->contains($element)) {
$element->setScale(null);
$em->persist($element);
$em->remove($element);
}
}
// Get elements
$scaleElements = $scale->getElements();
// Init counter
$counter = 1;
foreach ($scaleElements as $element) {
$element->setSorting($itemCounter);
$element->setScale($scale);
$em->persist($element);
$counter++;
}
$em->persist($scale);
$em->flush();
$this->get('session')->getFlashBag()->add('info', $this->get('translator')->trans('object edited', array()));
return $this->redirect($this->generateUrl('scale_list'));
}
return $this->render('AppMyBundle:Scale:edit.html.twig', array("form" => $form->createView()));
}
But the sorting attribute do not change from scale elements. It have the initial state which was set in the add action. In add action exists the same loop which set the sorting attribute for posted scale elements.
How can i solve this issue?

Related

Get scale unit at cart

I need the scale unit at my cart to show it at the frontend, but I do not get it.
I tried by a subscriber and to load the product, but I do not get the selected scale unit.
public function onLoadCart(OffcanvasCartPageLoadedEvent $event)
{
foreach ($event->getPage()->getCart()->getLineItems() as $item) {
$product = $this->productRepository->search(
new Criteria(
[
$item->getId()
]
),
$event->getContext()
)->first();
dd($product);
}
}
The term scale unit offset me a little at first. I assume you are looking for the ProductUnit. The ProductUnit is an association, and you need to add this to your criteria.
public function onLoadCart(OffcanvasCartPageLoadedEvent $event)
{
foreach ($event->getPage()->getCart()->getLineItems() as $item) {
$criteria = new Criteria(
[
$item->getId()
]
);
$criteria->addAssociation('unit');
$product = $this->productRepository->search(
$criteria,
$event->getContext()
)->first();
dd($product);
}
}

Get initial value of entity in FormExtension

In my update form, I want to add a data attribute on the inputs that will contains the initial value of the entity. This way, I will be able to highlight the input when the user will modify it.
In the end, only the input modified by the users will be highlighted.
I want to use this only in update, not in creation.
To do so, I created a form extension like this:
class IFormTypeExtension extends AbstractTypeExtension
{
...
public static function getExtendedTypes()
{
//I want to be able to extend any form type
return [FormType::class];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_iform' => false,
'is_iform_modification' => function (Options $options) {
return $options['is_iform'] ? null : false;
},
]);
$resolver->setAllowedTypes('is_iform', 'bool');
$resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!$options['is_iform'] && !$this->isParentIForm($form)) {
return;
}
//We need to add the original value in the input as data-attributes
if (is_string($form->getViewData()) || is_int($form->getViewData())) {
$originValue = $form->getViewData();
} elseif (is_array($form->getViewData())) {
if (is_object($form->getNormData())) {
$originValue = implode('###', array_keys($form->getViewData()));
} elseif (is_array($form->getNormData()) && count($form->getNormData()) > 0 && is_object($form->getNormData()[0])) {
$originValue = implode('###', array_keys($form->getViewData()));
} else {
$originValue = implode('###', $form->getViewData());
}
} else {
//There's no value yet
$originValue = '';
}
$view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}
private function isParentIForm(FormInterface $form)
{
if (null === $form->getParent()) {
return $form->getConfig()->getOption('is_iform');
}
return $this->isParentIForm($form->getParent());
}
}
As you can see in the buildView method, I get the originValue from the ViewData.
In a lot of cases, this works well.
But if I have any validation error in my form OR if I reload my form through AJAX, the ViewData contains the new information and not the values of the entity I want to update.
How can I get the values of the original entity?
I don't want to make a DB request in here.
I think I can use the FormEvents::POST_SET_DATA event, then save the entity values in session and use these in the buildView.
I could also give a new Option in my OptionResolver to ask for the initial entity.
Is it possible to have the original data of the entity directly form the buildView? (If I'm not wrong, this means the form before we call the handleRequest method).
Someone wanted to have an example with a controller. I don't think it's really interresting, because with the FormExtension, the code will be added automatically. But anyway, here is how I create a form in my controller :
$form = $this->createForm(CustomerType::class, $customer)->handleRequest($request);
And in the CustomerType, I will add the 'is_iform' key with configureOptions() :
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
"translation_domain" => "customer",
"data_class" => Customer::class,
'is_iform' => true //This line will activate the extension
]);
}
It's probably an opinionated answer. There also might be better approaches.
I'm not a big fan of your form extension, since it's really convoluted and unclear what's happening, at least to my eyes.
What I'm proposing: When the form submit happened, in your controller you should do the following
// ((*)) maybe store customer, see below
$form = $this->createForm(CustomerType::class, $customer);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
// easy case, you got this.
$em->flush();
return $this->redirect(); // or another response
} elseif($form->isSubmitted()) {
// form was submitted with errors, have to refresh entity!
// REFRESH - see discussion below for alternatives
$em->refresh($customer);
// then create form again with original values:
$form = $this->createForm(CustomerType::class, $customer);
}
// other stuff
return $this->render(..., ['form' => $form->createView(), ...]);
so, essentially, when the form validation fails, you refresh the entity and recreate the form, avoiding the problem with the changed state of your entity. I believe this approach ultimately is easier then hacking the form to magically not update values or re-set older values.
Now the question remains: how to refresh an entity? Simplest approach: reload from database:
$em->refresh($customer); // easiest approach, will likely run another query.
Alternatives:
Instead of giving $customer to the form, you create a customer DTO that contains the same values but on change doesn't automatically change the original object. If the form validation fails, you can just re-generate the DTO.
Instead of refresh($customer), which will most likely run another query (except maybe not, if you have a cache), you could cache the customer yourself via a DefaultCacheEntityHydrator, you would have to create your own EntityCacheKey object (not really hard), generate a cache entry (DefaultCacheEntityHydrator::buildCacheEntry() at the ((*)) above) and restore the entry for when you need to restore it. Disclaimer: I don't know if/how this works with collections (i.e. collection properties, the entity might have).
That being said ... if you really really want a form extension for whatever reason, you might want to form event with a PRE_SET_DATA handler that stores the data in the form type object, then on buildView uses those values. I wouldn't store something in the session for I don't see the necessity ... your aversion to db queries is baffling though, if that's your main reason for all the shenanigans
In the end, I managed to make it work BUT I'm not fully convinced by what I did.
It was not possible to get the original data from the form OR add a new property (the form is read only in the form extension).
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
if ('_token' === $form->getName()) {
return;
}
$data = $event->getData();
$this->session->set('iform_'.$form->getName(), is_object($data) ? clone $data : $data);
}
);
}
What I do here, is simply register the form values by its name in the session.
If it's an object, I need to clone it, because the form will modify it later in the process and I want to work with the original state of the form.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_iform' => false,
'is_iform_modification' => function (Options $options) {
return $options['is_iform'] ? null : false;
},
]);
$resolver->setAllowedTypes('is_iform', 'bool');
$resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}
The configure options did not change.
And then, depending on the value type, I create my "data-orig-value" :
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!$options['is_iform'] && !$this->isParentIForm($form)) {
return;
}
$propertyValue = $this->session->get('iform_'.$form->getName());
$originValue = '';
try {
if (null !== $propertyValue) {
//We need to add the original value in the input as data-attributes
if (is_bool($propertyValue)) {
$originValue = $propertyValue ? 1 : 0;
} elseif (is_string($propertyValue) || is_int($propertyValue)) {
$originValue = $propertyValue;
} elseif (is_array($propertyValue) || $propertyValue instanceof Collection) {
if (is_object($propertyValue)) {
$originValue = implode('###', array_map(function ($object) {
return $object->getId();
}, $propertyValue->toArray()));
} elseif (is_array($propertyValue) && count($propertyValue) > 0 && is_object(array_values($propertyValue)[0])) {
$originValue = implode('###', array_map(function ($object) {
return $object->getId();
}, $propertyValue));
} else {
$originValue = implode('###', $propertyValue);
}
} elseif ($propertyValue instanceof DateTimeInterface) {
$originValue = \IntlDateFormatter::formatObject($propertyValue, $form->getConfig()->getOption('format', 'dd/mm/yyyy'));
} elseif (is_object($propertyValue)) {
$originValue = $propertyValue->getId();
} else {
$originValue = $propertyValue;
}
}
} catch (NoSuchPropertyException $e) {
if (null !== $propertyValue = $this->session->get('iform_'.$form->getName())) {
$originValue = $propertyValue;
$this->session->remove('iform_'.$form->getName());
} else {
$originValue = '';
}
} finally {
//We remove the value from the session, to not overload the memory
$this->session->remove('iform_'.$form->getName());
}
$view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}
private function isParentIForm(FormInterface $form)
{
if (null === $form->getParent()) {
return $form->getConfig()->getOption('is_iform');
}
return $this->isParentIForm($form->getParent());
}
Maybe the code sample will help anyone !
If anyone have a better option, don't hesitate to post it !

EntityChangeSet in preUpdate not containing relations?

I am trying to do something when a User joins a Group. I am trying to use the preUpdate event for it and then check if the corresponding relations have changed. Unfortunately in my Group the 'users" relation is never in the changeset, as well as in my User the Usergroup is never in the changeset.
Here the two listeners:
public function preUpdate(PreUpdateEventArgs $args){
if($args->hasChangedField('users')){
$old = $args->getOldValue('users');
$new = $args->getNewValue('users');
}
}
public function preUpdate(PreUpdateEventArgs $args){
if($args->hasChangedField('userGroups')){
$old = $args->getOldValue('userGroups');
$new = $args->getNewValue('userGroups');
}
}
Thats my TestCase:
$group->addUser($user);
$em->beginTransaction();
$em->persist($group);
$em->flush();
$em->rollback();
Both listeners are called, but the relation is never in the changeset.
I want to add the user into a redis table, where I manage some specific data. Maybe the onFlush or some other events are better, since I don't need to modify the saved data. I just want to know if a there is a new Entry in my User-UserGroup Relation. I thought the easiest way to check this would be the changeset within the preUpdate function.
I solved it via the onFlush event:
public function onFlush(OnFlushEventArgs $args){
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledCollectionUpdates() as $col) {
if($this->isUserGroupUserAssociation($col->getMapping())){
$userGroup = $col->getOwner();
foreach($col->getInsertDiff() as $user){
$this->container->get('strego_user.user_manager')->triggerJoined($userGroup,$user);
}
}
}
}
protected function isUserGroupUserAssociation($association){
return($association['fieldName'] == "users" &&
$association['sourceEntity'] == "Strego\UserBundle\Entity\UserGroup"
);
}

Doctrine get accessor for entities

Is there anyway to access the data of my entity without to use a specific accessor to my column value. Is there any generic accessor? See example:
$em = $this->getDoctrine()->getManager();
$data = $em->getRepository('EgBundle:Table')->findAll()
foreach($data as $row) {
var_dump($row->get('col1')); // I would like to do this
var_dump($row->getCol1()); // instead of this
$col = 'getCol1'; var_dump($row->$col()); // this is my temporary solution
}
You might be able to make use of the symfony2 PropertyAccessor component.
Docs here: http://symfony.com/doc/current/components/property_access/introduction.html
Example:
$accessor = PropertyAccess::createPropertyAccessor();
$accessor->getValue($row, 'col1');
You can get an array if you use DQL
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery('SELECT table FROM EgBundle\Entity\Table table');
$data = $query->getQuery()->getArrayResult();
foreach ($data as $row) {
var_dump($data['col1']);
}
No, not without adding support for that with userland code. You can add something like:
public function get($property) {
return $this->$property;
}

unique slugs with multi entities in symfony2

I have a small symfony2 application where a user can create pages. Each page should be accessible via the route /{user_slug}/{page_slug}. I have the entities user and page and I use the sluggable behavior for both entities. In order to find the correct page the combination of user_slug and page_slug has to be unique.
What is the best way to check that the combination of user_slug and page_slug is uniqe?
Try this in your prepository:
public function findByUsernameAndSlug($username, $slug)
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT g
FROM Acme\PagesBundle\Entity\Page p
JOIN p.owner u
WHERE u.username = :username
AND p.slug = :slug
")
->setParameter('username', $username)
->setParameter('slug', $slug);
foreach ($query->getResult() as $goal) {
return $goal;
}
return null;
}
Before you persist the entity in your service-layer check whether given combination of user and page slug are unique, if not modify the page slug (append -2 or something like that) or throw an exception:
public function persistPage(Page $page) {
$userSlug = $page->getUser()->getSlug();
$pageSlug = $page->getSlug();
if ($this->pagesRepository->findOneBySlugs($userSlug, $pageSlug) != null) {
// given combination already exists
throw new NonUniqueNameException(..);
// or modify the slug
$page->setSlug($page->getSlug() . '-2');
return $this->persistPage($page);
}
return $this->em->persist($page);
}
// PagesRepository::findOneBySlugs($userSlug, $pageSlug)
public function findOneBySlugs($userSlug, $pageSlug) {
$query = $this->em->createQueryBuilder('p')
->addSelect('u')
->join('p.user', 'u')
->where('p.slug = :pageSlug')
->where('u.slug = :userSlug;)
->getQuery();
$query->setParameters(combine('userSlug', 'pageSlug'));
return $query->getSingleResult();
}

Resources