Symfony2 - set an entity so its properties are "read only" - symfony

We're using Symfony 2.3 and Doctrine to manage entities.
Suppose we've an entity called "Task" which a user can create as usual using the Symfony form builder and can persist using Doctrine. The "Task" might be an instruction to send some emails on a certain date, for example. Suppose the "Task" has a property called "status". "Tasks" that have been created correctly might have a status of "Ready".
A cron job periodically calls a symfony Custom Command to check if there are any "Tasks" that need processing (i.e. have a status of "Ready") and if it finds any, it performs some action and then updates the entity to have a status of "Completed".
Once a Task has been processed and its status has been set to be "Completed", is there any way of making the entity effectively "read only"? By this, I mean that a user could still see details of the Task, but they would not be able to change of its properties using a form? Bear in mind that the user might browse to the "edit" form for a particular Task, and whilst they are reading the details, the cron job might process the task and update the "status" property of the Task - the user might then submit the form without knowing that the Task has already been processed, and the form handler could attempt to persist the entity, possibly setting the "status" back to be "Ready" - thereby ensuring that the same task gets processed again the next time the cron job runs.
If, however, the Task was made read-only when its status gets set to be "Completed", the form submission would have no effect.

You should do this in your CRUD. Upon calling the "editAction" you must check the status on the entity. If it is "Completed" then you wouldn't display the edit form, but rather redirect the user to the "showAction"
For example:
/**
* Displays a form to edit an existing Task entity.
*
* #Route("/{id}/edit", name="task_edit")
* #Method("GET")
* #Template()
*/
public function editAction($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AcmeTaskBundle:Task')->find($id);
if($entity->isCompleted())
return $this->forward('AcmeTaskBundle:Task:show', array('id' => $id ));
//Else finish edit action
}
/**
* Finds and displays a Task entity.
*
* #Route("/{id}", name="task_show")
* #Method("GET")
* #Template()
*/
public function showAction($id) {
...
}
Additionally, you can do the same thing in your update Action once you have checked the validity of the form. Just in case they have the edit form open right before the cron is run.
/**
* Edits an existing Task entity.
*
* #Route("/{id}", name="task_update")
* #Method("PUT")
* #Template("AcmeTaskBundle:Task:edit.html.twig")
*/
public function updateAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AcmeTaskBundle:Task')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Task entity.');
}
if($entity->isCompleted())
//Redirect Again. Note that this happens BEFORE the flush() which is when the entity is persisted to the db
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
if ($editForm->isValid()) {
$em->flush();
return $this->redirect($this->generateUrl('administration_product_edit', array('id' => $id)));
}
...
}
Furthermore, if the Task is completed, they you should probably remove the "Edit option" from the showAction user interface all together. That would mean, in your twig template you check if the entity is completed BEFORE rendering the "Edit button"

Related

FOSUserBundle, EventListener registration user

I'm working on the FOSUserBundle, on EventListener for RegistrationUser.
In this bundle, when I create a user, I use a method updateUser() (in Vendor...Model/UserManagerInterface). This method seems to be subject to an EventListener that triggers at least two actions. Registering the information entered in the database. And sending an email to the user to send them login credentials.
I found the method that sends the mail. By cons, I didn't find the one who makes the recording. I also didn't find where to set the two events.
First for all (and my personal information), I try to find these two points still unknown. If anyone could guide me?
Then, depending on what we decide with our client, I may proceed to a surcharge (which I still don't really know how to do), but I imagine that I would find a little better once my two strangers found
Thanks for your attention and help
This is the function wich handles the email confirmation on registrationSucces
FOS\UserBundle\EventListener\EmailConfirmationListener
public function onRegistrationSuccess(FormEvent $event)
{
/** #var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$user->setEnabled(false);
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
$this->mailer->sendConfirmationEmailMessage($user);
$this->session->set('fos_user_send_confirmation_email/email', $user->getEmail());
$url = $this->router->generate('fos_user_registration_check_email');
$event->setResponse(new RedirectResponse($url));
}
But I tell you that what you are trying to do is a bad practice. The recommended way is the following.
Step 1: Select one of the following events to listen(depending on when you want to catch the process)
/**
* The REGISTRATION_SUCCESS event occurs when the registration form is submitted successfully.
*
* This event allows you to set the response instead of using the default one.
*
* #Event("FOS\UserBundle\Event\FormEvent")
*/
const REGISTRATION_SUCCESS = 'fos_user.registration.success';
/**
* The REGISTRATION_COMPLETED event occurs after saving the user in the registration process.
*
* This event allows you to access the response which will be sent.
*
* #Event("FOS\UserBundle\Event\FilterUserResponseEvent")
*/
const REGISTRATION_COMPLETED = 'fos_user.registration.completed';
Step 2 Implement the Event Subscriber with a priority
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => [
'onRegistrationSuccess', 100 //The priority is higher than the FOSuser so it will be called first
],
);
}
Step 3 Implement your function
public function onRegistrationSuccess(FormEvent $event)
{
//do your logic here
$event->stopPropagation();//the Fos User method shall never be called!!
$event->setResponse(new RedirectResponse($url));
}
You never should modify the third party libraries in this case the Event Dispatcher System is made for this to earlier process the event and if its needed stop the propagation and avoid the "re-processing" of the event.
Hope it helps!!!!

Symfony 3 - Update Many-To-Many

I have been looking around for a clean solution on how to update (keep in sync) a many to many relationship?
I have the following scenario:
A Sprint Entity owns the Many To Many relationship towards the Ticket entity.
When editing a Ticket (or Sprint, but I am not there yet), I want to be able to select (checkboxes) the Sprints that this ticket belongs to.
Upon persistance (save), I want to update my join table tickets_sprint (which is just a join table on ticket_id, sprint_id).
Adding Sprints to the Ticket seems easy enough, but removing Sprints from the Ticket is not reflected at all.
Code
Ticket Entity contains this method for adding a Ticket to a Sprint:
public function setSprints($sprints) {
/**
* #var $sprint \AppBundle\Entity\Sprint
*/
foreach ($sprints as $sprint) {
$this->sprints[] = $sprint;
$sprint->addTicket($this);
}
}
I have read here that the only way to go would be to remove all relations and re-save them upon persistance.
Coming from the Laravel world, this hardly feels like a good idea :)
This is how it is done in Laravel:
/**
* #param \App\User $user
* #param \App\Http\Requests\StoreUserRequest $request
* #return \Illuminate\Http\RedirectResponse
* Update the specified resource in storage.
*/
public function update(User $user, StoreUserRequest $request)
{
$user->fill($request->input());
$user->employee_code = strtolower($user->employee_code);
$user->roles()->sync($request->role ? : []);
$user->save();
\Session::flash('flash_message_success', 'The user was successfully updated.');
return redirect()->route('frontend::users.show', [$user]);
}
All suggestions are welcome!
The EntityType that you may use to create a multiple selectbox doesn't have a by_reference option like CollectionType.
If your Ticket Entity use the "inversedBy" side, you don't need to add the reference in the other object. So you can symply do this :
public function setSprints($sprints) {
$this->sprints = $sprints;
}
Maybe this will be enough to add and remove your elements automatically (Sorry didn't try).
Otherwise you have to do it manually and you can create a new method to remove elements returns by the difference between your new ArrayCollection and the old one.

Doctrine Events: How can I track additions / removals to a ManyToMany collection?

I have an Application entity that has a ManyToMany relationship to the SortList entity. The owning side is Application. There's a simple join table that creates the mapping for this relationship.
Here's how the Application entity looks with regards to managing the collection:
/**
* Add sortLists
*
* #param \AppBundle\Entity\SortList $sortList
* #return Application
*/
public function addSortList(SortList $sortList)
{
$this->sortLists[] = $sortList;
$sortList->addApplication($this);
return $this;
}
/**
* Remove sortLists
*
* #param \AppBundle\Entity\SortList $sortList
*/
public function removeSortList(SortList $sortList)
{
$this->sortLists->removeElement($sortList);
$sortList->removeApplication($this);
}
/**
* Get sortLists
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getSortLists()
{
return $this->sortLists;
}
I want to track when SortLists have been added or removed from an Application.
I've already learned that I can't use postUpdate lifecycle event to track these changes collections.
Instead, it seems I should use onFlush and then $unitOfWork->getScheduledCollectionUpdates() and $unitOfWork->getScheduledCollectionDeletions().
For updates, I see I can use the "internal" method getInsertDiff to see which items in the collection were added and getDeleteDiff to see which items in the collection were removed.
But I have a couple concerns:
If all items in the collection were removed, there's no way to see which items were actually removed since $unitOfWork->getScheduledCollectionDeletions() doesn't have this information.
I'm using methods that are marked as "internal"; it seems like they could "disappear" or be refactored some point in the future without me knowing?
I solved this empty getDeleteDiff in https://stackoverflow.com/a/75277337/5418514
The reason this is sometimes empty is an old but still existing problem. The solution at the moment is to fetch the data again yourself.
public function onFlush(OnFlushEventArgs $args)
{
$uow = $args->getEntityManager()->getUnitOfWork();
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
/**
* "getDeleteDiff" is not reliable, collection->clear on PersistentCollection also clears the original snapshot
* A reliable way to get removed items is: clone collection, fetch original data
*/
$removedData = $collection->getDeleteDiff();
if (!$removedData) {
$clone = clone $collection;
$clone->setOwner($collection->getOwner(), $collection->getMapping());
// This gets the real data from the database into the clone
$uow->loadCollection($clone);
// The actual removed items!
$removedData = $clone->toArray();
}
}
}
I think the examples below cover everything you would need so you just need to implement which ever you want/need in your app.
For tracking persist operations, you can use prePersist and
postPersist event listener on an entity or prePersist and
postPersist event subscriber on an entity examples. PrePersist
won't give you the ID cos it doesn't exist in DB yet whereas
PostPersist will as shown in the example.
For tracking remove operations, you can use preRemove and
postRemove event listener on an entity example.
For tracking update operations which is the tricky one, you can use
preUpdate event listener on an entity example but pay attention
how it is done.
For inserting, updating and removing operations, you can use
onFlush event listener on an entity example which covers
UnitOfWork getScheduledEntityInsertions,
getScheduledEntityUpdates and getScheduledEntityDeletions
methods.
There are many other useful listener examples in that website so just use search feature for listener keyword. Once I did same thing as you wanted for M-N associations but cannot find the example. If I can I'll post it but not sure if I can!

Remove all related children in Symfony entity

Suppose we have a field with ManyToMany relation as
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Users")
* #ORM\JoinTable(name="users_roles",
* joinColumns={#ORM\JoinColumn(name="User_Id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="Role_Id", referencedColumnName="id")})
*/
protected $userRole;
To remove one related element from table we can have this function in our entity:
/**
* Remove userRole
* #param \Acme\MyBundle\Entity\Users $user
*/
public function remvoveUserRole(\Acme\MyBundle\Entity\Users $user)
{
$this->userRole->removeElement($user);
}
The Question:
The ArrayCollection type has the function removeElement which is used to remove one element of the relationship. There is another function clear which in api says Clears the collection, removing all elements, therefore can I have a function like below in my entity to clear all the related elements so that by flushing it removes all?
/**
* Remove all user Roles
*/
public function remvoveAllUserRole()
{
$this->userRole->clear();
}
will it work for just ManyToMany related tables or it might work for ManyToOne, too?
Ţîgan Ion is right - removeElement/clear only removes those elements from memory.
However, I think you could achieve something as close depending on how did you configure cascade and orphanRemoval in your relationship.
$em = ...; // your EntityManager
$roles = $user->getRoles();
$roles->clear();
$user = $em->merge($user); // this is crucial
$em->flush();
In order for this to work, you need to configure User relationship to
cascade={"merge"} - this will make $em->merge() call propagate to roles.
orphanRemoval = true - since this is #ManyToMany, this will make EntityManager remove free-dangling roles.
Can't test this now, but as far as I can see it could work. I will try this out tomorrow and update the answer in need be.
Hope this helps...
Note: This logic works for ManyToMany relationship, but not for ManyToOne
I tested the way to delete all related roles for specific use (ManyToMany) and it worked. What you need is to define a function in your UserEntity as
/**
* Remove all user Roles
*/
public function remvoveAllUserRole()
{
$this->userRole->clear();
}
Then in your controller or anywhere else(if you need) you can call the function as below
$specificUser = $em->getRepository('MyBundle:Users')->findOneBy(array('username' => 'test user'));
if (!empty($specificUser)) {
$specificUser->removeAllUserRole();
$em->flush();
}
Then it will delete all related roles for the test user and we don't need to use the for loop and remove them one by one
if I'm not mistaken, this will not work, you have to delete the "role
s" from the arrayCollection directly from the database
$roles = $user->getRoles()
foreach $role from $roles
$em->remove($role);
$em->flush();
now you should get an empty collection
p.s: the best way is to test your ideas

OneToMany relationship not persisting along with new entities

I'm facing some issue when I try to persist a collection of entities using a symfony form. I followed the official documentation but I can't make it work becouse of this error:
Entity of type ProductItem has identity through a
foreign entity Product, however this entity has no identity itself. You have to call
EntityManager#persist() on the related entity and make sure that an identifier was
generated before trying to persist ProductItem. In case of Post Insert ID
Generation (such as MySQL Auto-Increment or PostgreSQL SERIAL) this means you
have to call EntityManager#flush() between both persist operations.
I have to entities linked with a OneToMany relation:
Product
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="ProductItem", mappedBy="product",cascade={"persist"})
*/
protected $items;
And ProductItem
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Product", inversedBy="items")
*/
protected $product;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Item")
*/
protected $item;
This is how it is added to the form:
->add('items','collection',array(
'label' => false,
'type' => new ProductItemType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false))
And this is the controller action:
public function newAction()
{
$product= new Product();
$form = $this->createForm(new ProductType(), $product);
if($request->isMethod("POST"))
{
$form->handleRequest($request);
if($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
}
}
}
I'm doing something wrong for sure in the controller because, as the error message says, I have to persist $product before adding $productItems, but how can I do that?
I only get this error when trying to persist a new entity, if the entity has been persisted before, I can add as may items as I want successfully
I had exact same problem last week, here is a solution I found after some reading and testing.
The problem is your Product entity has cascade persist (which is usually good) and it first try to persist ProductItem but ProductItem entities cannot be persisted because they require Product to be persisted first and its ID (Composite key (product, item).
There are 2 options to solve this:
1st I didn't use it but you could simply drop a composite key and use standard id with foreign key to the Product
2nd - better This might look like hack, but trust me this is the best what you can do now. It doesn't require any changes to the DB structure and works with form collections without any problems.
Code fragment from my code, article sections have composite key of (article_id, random_hash). Temporary set one to many reference to an empty array, persist it, add you original data and persist (and flush) again.
if ($form->isValid())
{
$manager = $this->getDoctrine()->getManager();
$articleSections = $article->getArticleSections();
$article->setArticleSections(array()); // this won't trigger cascade persist
$manager->persist($article);
$manager->flush();
$article->setArticleSections($articleSections);
$manager->persist($article);
$manager->flush();
You didn't follow the docs completely. Here is something you can do to test a single item, but if you want to dynamically add and delete items (it looks like you do), you will also need to implement all the javascript that is included in the docs that you linked to.
$product= new Product();
$productItem = new ProductItem();
// $items must be an arraycollection
$product->getItems()->add($productItem);
$form = $this->createForm(new ProductType(), $product);
if($request->isMethod("POST"))
{
$form->handleRequest($request);
if($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($productItem);
$em->persist($product);
$em->flush();
}
}
So this should work for a single static item, but like I said, the dynamic stuff is a bit more work.
The annotation is wrong... the cascade persist is on the wrong side of the relation
/**
* #ORM\OneToMany(targetEntity="ProductItem", mappedBy="product")
*/
protected $items;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Product", inversedBy="items", cascade={"persist"})
*/
protected $product;
Another way to achieve this (e.g. annotation not possible) is to set the form by_reference
IMO, your problem is not related to your controller but to your Entities. It seems your would like to make a ManyToMany between your Product and Item and not creating a ProductItem class which should behave as an intermediate object for representing your relation. Additionally, this intermediate object have no id generation strategy. This is why Doctrine explains you, you must first persist/flush all your new items and then persist/flush your product in order to be able to get the ids for the intermediate object.
Also faced this issue during the work with form to which CollectionType field was attached. The other one approach which could solve this problem and also mentioned in doctrine official documentation is following:
public function newAction()
{
$product= new Product();
$form = $this->createForm(new ProductType(), $product);
if($request->isMethod("POST"))
{
$form->handleRequest($request);
if($form->isValid())
{
foreach ($product->getItems() as $item)
{
$item->setProduct($product);
}
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
}
}
}
In simple words, you should provide product link to linked items manually - this is described in "Establishing associations" section of following article: http://docs.doctrine-project.org/en/latest/reference/working-with-associations.html#working-with-associations

Resources