api-platform unwanted field update on custom operation - symfony

There is an app with api-platform on top of symfony 4. I have created a custom operation as recommended.
The Entity
* #Api\ApiResource(
* normalizationContext={"groups"={"read_importation_demand"}},
* denormalizationContext={"groups"={"write_importation_demand"}},
* itemOperations={
* "get",
* "put",
* "delete",
* "workflow"={
* "method"="POST",
* "path"="/importation_demands/{id}/workflow",
*
* "controller"=ImportationWorkflowController::class,
* }
* })
The Controller
public function __invoke(ImportationDemand $data, Request $request): ImportationDemand
{
$requestData = json_decode($request->getContent(), true);
$request->request->replace($requestData);
$workflow = $this->workflowsRegistry->get($data, 'presentacion_oferta');
$transicion = $request->get('transition');
$workflow->apply($data, constant("App\Enum\ImportationDemandWorkflowEnum::$transicion"));
$this->manager->flush();
return $data;
}
What I'm doing here is receiving a description and transition parameters to set on the workflow entity. The problem is that when I flush to database the description attribute in ImportationDemand is setted with the value of the description field received. It suposed to be setted only in the ImportationDemandWorkflow entity but is settedin both. As you can see in the code I never set the ImportationDemand description field.
Transition event listener
public function onWorkflowPresentacionOfertaEntered(WorkflowEvent $event)
{
$request = $this->container->get('request_stack')->getCurrentRequest();
$username = $this->container->get('security.token_storage')->getToken()->getUsername();
$user = $this->manager->getRepository(User::class)->loadUserByUsername($username);
/** #var ImportationDemand $solicitud */
$solicitud = $event->getSubject();
$workflow = new ImportationDemandWorkflow();
$workflow->setStatus($solicitud->getStatus())
->setDescription($request->get('description', 'No se introdujo una descripción para esta operación'))
->setAction($event->getTransition()->getName())
->setDoneAt(new \DateTime())
->setPerformer($user);
$solicitud->addWorkflow($workflow);
}
The spected behavior is that the description field would be setted only in the entity ImportationDemandWorkflow.
Why is api-platform updating the description field if I'm, allegendly, taking control of the performed operation?
Thanks in advance!

Related

Symfony Serializer with Groups not working - empty output

I am trying to serialise data as JSON with the default Symfony Serializer.
To do that I'm trying to use #Groups() as explained here:
https://symfony.com/doc/current/serializer.html
After adding the #Groups annotation as shown below:
class User implements UserInterface
{
// ...
/**
* #ORM\OneToMany(targetEntity=PortfolioItem::class, mappedBy="user", orphanRemoval=true)
* #ORM\OrderBy({"id" = "DESC"})
* #Groups({"show_user"})
*/
private $portfolioItems;
}
On my controller I have the following:
/**
* #param Request $request
* #return JsonResponse
* #Route("/async/portfolio/brands/get_chart", name="portfolio.brands.chart.data", options={"expose"=true}, methods={"POST", "GET"})
* #IsGranted("ROLE_USER")
*/
public function getDataForBrandsChart(Request $request): JsonResponse
{
$user = $this->getUser();
$portfolioItems = $user->getPortfolioItems();
$output = $this->serializer->serialize($portfolioItems, "json", ["groups" => "show_user"]);
return new JsonResponse($output, 200);
}
This always gives the following output:
[[]]
Why is it always empty?
The reason I am using the Groups is because without them I have the following error:
A circular reference has been detected when serializing the object of class "App\Entity\PortfolioItem" (configured limit: 1).
The problem was cache.
Restarting the server after the extra-bundle composer installation and running bin/console cache:clear solved the issue.

API Platform - How to Use a DTO for Posting?

I'm using API platform in my Symfony4 app to expose my resources.
It's a great framework but it force you by default to have all your Business logic in the front-end side, because it expose all your Entities and not a Business Object.
I don't like that and I prefer to have my business logic in the back-end side.
I need to create users, but there are different type of users.
So I have create a UserFactory in the back-end-side. So the front just need to push a Business object and the back-end take care of everything.
The front front can never persist a User Object directly in the DB. It is the role of the back-end
Following this tutorial to use DTO for Reading:
https://api-platform.com/docs/core/dto/#how-to-use-a-dto-for-reading
I'm trying to do the same for posting. And it works. Here is my Controller code:
/**
* #Route(
* path="/create/model",
* name="create-model",
* methods={"POST"},
* defaults={
* "_api_respond"=true,
* "_api_normalization_context"={"api_sub_level"=true},
* "_api_swagger_context"={
* "tags"={"User"},
* "summary"="Create a user Model",
* "parameters"={
*
* },
* "responses"={
* "201"={
* "description"="User Model created",
* "schema"={
* "type"="object",
* "properties"={
* "firstName"={"type"="string"},
* "lastName"={"type"="string"},
* "email"={"type"="string"},
* }
* }
* }
* }
* }
* }
* )
* #param Request $request
* #return \App\Entity\User
* #throws \App\Exception\ClassNotFoundException
* #throws \App\Exception\InvalidUserException
*/
public function createModel(Request $request)
{
$model = $this->serializer->deserialize($request->getContent(), Model::class, 'json');
$user = $this->userFactory->create($model);
$this->userRepository->save($user);
return $user;
}
It works great, but I would love my new resource to work in the Swagger UI, so I can Create via POST method new resources directly in the web interface.
For that I think I need to complete the parameter section in my _api_swagger_context. But I don't fin any documentation about that.
How can I do that?
Found the answer here: https://github.com/api-platform/docs/issues/666
You can fill parameters like this :
"parameters" = {
{
"name" = "data",
"in" = "body",
"required" = "true",
"schema" = {
"type" = "object",
"properties" = {
"firstName"={"type"="string"},
"lastName"={"type"="string"},
"email" = {"type" = "string" }
}
},
},
},
More docs about parameters for swagger here : https://swagger.io/docs/specification/2-0/describing-parameters/

Paypal IPN not working, but posting data does

I am using the PayPal IPN simulator here: https://developer.paypal.com/webapps/developer/applications/ipn_simulator
to send information to an application built with symfony2 and payum bundle (older version of symfony and bundle).
It is definitely getting to the application at the notify URL (so not a firewall issue) because a record is stored in the database with the payment name and the date. However there are no 'details' stored.
However, if I use a Rest Client to POST to a URL with data, as suggested here: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNIntro/#id08CKFJ00JYK
Then the record is stored with payment name and date and details!!
Is this an issue with the IPN simulator? I'm really not sure what is going on here, maybe I could try and log the request object somehow?
#hsb1007 I think this was what I used finally. But I'm pretty sure there was some settings on the paypal side which was the main issue. I just remember doing lots and lots of testing and waiting
<?php
namespace LabIT\CMSBundle\EventListener;
use Buzz\Client\ClientInterface;
use Exception;
use LabIT\CMSBundle\Entity\Payments\DetailsInterface;
use LabIT\CMSBundle\Helper\ApiHelper;
use LabIT\CMSBundle\Helper\PaymentHelper;
use LabIT\CMSBundle\Helper\SubscriptionHelper;
use LabIT\CMSBundle\Helper\UserHelper;
use Payum\Core\Action\PaymentAwareAction;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\Notify;
use Payum\Paypal\Ipn\Api;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
class PaymentListener extends PaymentAwareAction
{
/**
* #var UserHelper
*/
protected $userHelper;
/**
* #var PaymentHelper
*/
protected $paymentHelper;
/**
* #var ApiHelper
*/
protected $apiHelper;
/**
* #var SubscriptionHelper
*/
protected $subscriptionHelper;
/**
* #var LoggerInterface
*/
protected $logger;
/**
* #var
*/
protected $buzz;
/**
* #var
*/
protected $sandbox;
/**
* #var
*/
protected $paypalValidation;
/**
* #param UserHelper $userHelper
* #param PaymentHelper $paymentHelper
* #param ApiHelper $apiHelper
* #param SubscriptionHelper $subscriptionHelper
* #param LoggerInterface $logger
* #param ClientInterface $buzz
* #param $sandbox
* #param $paypalValidation
*/
public function __construct(
UserHelper $userHelper,
PaymentHelper $paymentHelper,
ApiHelper $apiHelper,
SubscriptionHelper $subscriptionHelper,
LoggerInterface $logger,
ClientInterface $buzz,
$sandbox,
$paypalValidation
) {
$this->userHelper = $userHelper;
$this->paymentHelper = $paymentHelper;
$this->apiHelper = $apiHelper;
$this->subscriptionHelper = $subscriptionHelper;
$this->logger = $logger;
$this->buzz = $buzz;
$this->sandbox = $sandbox;
$this->paypalValidation = $paypalValidation;
}
/**
* {#inheritDoc}
*
* This is where all the IPNs from paypal get processed, acts in some ways like a controller
*
* #param Notify $request
*/
public function execute($request)
{
$data = $_POST;
// would be better to get this dynamically. It is the payment name defined in config,
// but also forms part of the url set in the paypal notification backend
$paymentName = 'post_a_job_with_paypal'; // todo maybe get this from the url
$this->logger->notice('IPN received');
// validate with paypal so it stops notifying (do this first because needs to be done within 30 seconds)
if (true === $this->paypalValidation) {
$this->validateWithPaypal($this->getPaypalArray());
}
$notificationDetails = $this->paymentHelper->createPaymentNotification($paymentName, $data);
// todo other inspections of data? check email?
$user = $this->paymentHelper->getNotificationUser($notificationDetails, $data);
// these are only done for individual transactions //TODO STORE TRANSACTIONS IN TABLE?
if (isset($data['txn_id'])) {
$this->paymentHelper->getTransactionProcessed($data['txn_id']); // so don't process more than once //TODO ADD BACK IN AFTER ADDING TRANSACTION CLASS
$this->subscriptionHelper->determineUserMembership($user, $notificationDetails); // automatically demote if payment fails
$this->apiHelper->sendPaymentNotifications($user, $notificationDetails); // notify affiliates
$this->paymentHelper->setTransactionProcessed($data['txn_id']); //set transaction to processed //TODO ADD BACK IN AFTER ADDING TRANSACTION CLASS
}
// handle recurring payment data (recurring payment info, but not recurring payment transaction
if (isset($data['recurring_payment_id']) && !isset($data['txn_id'])) {
$this->paymentHelper->setRecurringPaymentStatus($data);
// cron job will determine user membership level because needs to check timestamp
}
}
/**
* {#inheritDoc}
*/
public function supports($request)
{
return $request instanceof Notify;
}
/**
* Send data back to paypal so paypal knows it was delivered successfully
*
* #param array $data
*
* #throws Exception
*/
protected function validateWithPaypal(array $data)
{
$this->logger->notice('I am here');
$options = array();
$options['sandbox'] = $this->sandbox;
$api = new Api($this->buzz, $options);
// Verify the IPN via PayPal
if (Api::NOTIFY_VERIFIED !== $api->notifyValidate($data)) {
$this->logger->notice('paypal validation UNSUCCESSFUL');
throw new Exception('Invalid IPN');
}
$this->logger->notice('paypal validation SUCCESSFUL');
return;
}
/**
* #return array
*/
protected function getPaypalArray()
{
// Read POST data
// reading posted data directly from $_POST causes serialization
// issues with array data in POST. Reading raw POST data from input stream instead.
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode ('=', $keyval);
if (count($keyval) == 2)
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
return $myPost;
}

Sonata User Admin - Custom field dependency

I have extended the SonataAdmin class for FOSUser and added 2 custom fields (choice type from external data source): Company and Sector
I'd like to make Sector dependent on Company, so if the user selects a Company it filters the available Sectors.
I though about using FormEvents for filtering at page load, but I don't even know how to get the Company value of the current form.
Here is a part of my custom SectorType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA
, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
// Need to get the company value here if set
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'choices' => $this->getSectors(),
));
}
public function getSectors()
{
$sects = array();
// Need to pass the selected company value to my getList
// (which gets the list of sector as you can imagine)
if (($tmp_sects = $this->ssrs->getList('Sector'))) {
foreach ($tmp_sects as $sect) {
$label = $sect['id'] ? $sect['label'] : '';
$sects[$sect['id']] = $label;
}
}
return $sects;
}
So the question is:
How to get the selected Company from my custom SectorType ?
After that I'll need to be able to refresh the Sector with Ajax, but that will be another question
I had a similar problem. I needed to create a sale entity that needed to be associated in a many to one relationship with an enterprise entity and a many to many relationship with services entities. Here is the Sale Entity:
The thing is that services where available depending on the companies chosen. For instance services a and b could only be provided to company x. And services b and c could only be provided to company y. So in my admin, depending on the chosen company I had to display the available services. For these I needed to do 2 things:
First create a dynamic form with my sale admin, so that on the server side I could get the right services available for the company specified in my sale record. And second, I had to create a custom form type for my company form element, so that when it was changed by the user on the client side, It would send an ajax request to get the right services for the company chosen.
For my first problem, I did something similar to what you were trying to achieve, but instead of creating an specific custom type for my services element, I added de event listener directly in the admin.
Here is the Sale entity:
/**
*
* #ORM\Table(name="sales")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Sale
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
public $id;
/**
* #ORM\ManyToOne(targetEntity="Branch")
* #ORM\JoinColumn(name="branch_id", referencedColumnName="id", nullable = false)
* #Assert\NotBlank(message = "Debe especificar una empresa a la cual asignar el precio de este exámen!")
*/
private $branch;
/** Unidirectional many to many
* #ORM\ManyToMany(targetEntity="Service")
* #ORM\JoinTable(name="sales_services",
* joinColumns={#ORM\JoinColumn(name="sale_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="service_id", referencedColumnName="id")}
* )
* #Assert\Count(min = "1", minMessage = "Debe especificar al menos un servicio a realizar!")
*/
private $services;
public function __construct() {
$this->services = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set branch
*
* #param Astricom\NeurocienciasBundle\Entity\Branch $branch
*/
//default value always have to be null, because when validation constraint is set to notblank,
//if default is not null, before calling the validation constraint an error will pop up explaining
//that no instance of Branch was passed to the $branch argument.
public function setBranch(\Astricom\NeurocienciasBundle\Entity\Branch $branch = null)
{
$this->branch = $branch;
}
/**
* Get branch
*
* #return Astricom\NeurocienciasBundle\Entity\Branch
*/
public function getBranch()
{
return $this->branch;
}
/**
* Add service
*
* #param \Astricom\NeurocienciasBundle\Entity\Service|null $service
*/
public function addServices(\Astricom\NeurocienciasBundle\Entity\Service $service = null)
{
$this->services[] = $service;
}
/**
* Get services
*
* #return Doctrine\Common\Collections\Collection
*/
public function getServices()
{
return $this->services;
}
/**
* Sets the creation date
*
* #param \DateTime|null $createdAt
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
}
/**
* Returns the creation date
*
* #return \DateTime|null
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Sets the last update date
*
* #param \DateTime|null $updatedAt
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
}
So then in the Admin form builder:
protected function configureFormFields(FormMapper $formMapper) {
$em = $this->container->get('doctrine')->getEntityManager();
$branchQuery = $em->createQueryBuilder();
$branchQuery->add('select', 'b')
->add('from', 'Astricom\NeurocienciasBundle\Entity\Branch b')
->add('orderBy', 'b.name ASC');
$formMapper
->with('Empresa/Sucursal')
->add('branch','shtumi_ajax_entity_type',array('required' => true, 'label'=>'Empresa/Sucursal','error_bubbling' => true, 'empty_value' => 'Seleccione una empresa/sucursal', 'empty_data' => null, 'entity_alias'=>'sale_branch', 'attr'=>array('add_new'=>false), 'model_manager' => $this->getModelManager(), 'class'=>'Astricom\NeurocienciasBundle\Entity\Branch', 'query' => $branchQuery))
->end()
;
$builder = $formMapper->getFormBuilder();
$factory = $builder->getFormFactory();
$sale = $this->getSubject();
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function(DataEvent $event) use ($sale,$factory, $em) {
$form = $event->getForm();
$servicesQuery = $em->createQueryBuilder();
$servicesQuery->add('select','s')
->add('from','Astricom\NeurocienciasBundle\Entity\Service s');
if (!$sale || !$sale->getId()) {
$servicesQuery
->where($servicesQuery->expr()->eq('s.id', ':id'))
->setParameter('id', 0);
}
else {
$servicesQuery
->join('s.branch', 'b')
->where($servicesQuery->expr()->eq('b.id', ':id'))
->setParameter('id', $sale->getBranch()->getId());
}
$form->add($factory->createNamed('services','entity',null,array('required' => true, 'label'=>'Servicios','error_bubbling' => true, 'attr'=>array('show_value_label'=>true),'class'=>'Astricom\NeurocienciasBundle\Entity\Service','multiple'=>true,'expanded'=>true,'query_builder'=>$servicesQuery)));
}
);
}
The trick thing was to pass the forms data. It doesn't work to use evet->getData() in the event listener's function. Instead I passed it through the admin->getSubject() method. Then instead of adding a sonataadmin form type, inside the event listener's function, I had to use a plain symfony form type.
The Ajax part as you mentioned is another question. All the weird things on the branch add method in the form builder is related to a customized field type for this matter. Don't worry about it.

Symfony 2 - ManyToOne Bidirectional relationship behaviour

I had a big time trying to figure out how to setup a ManyToOne -> OneToMany relationship with Doctrine 2 and it still not working...
Here is the application behaviour:
A site has Pages
A User can write Comment on a Page
Here are my Entities (simplified):
Comment Entity:
**
* #ORM\Entity
* #ORM\Table(name="comment")
*/
class Comment {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Many Comments have One User
*
* #ORM\ManyToOne(targetEntity="\Acme\UserBundle\Entity\User", inversedBy="comments")
*/
protected $user;
/**
* Many Comments have One Page
*
* #ORM\ManyToOne(targetEntity="\Acme\PageBundle\Entity\Page", inversedBy="comments")
*/
protected $page;
...
/**
* Set user
*
* #param \Acme\UserBundle\Entity\User $user
* #return Comment
*/
public function setUser(\Acme\UserBundle\Entity\User $user)
{
$this->user = $user;
return $this;
}
/**
* Set page
*
* #param \Acme\PageBundle\Entity\Page $page
* #return Comment
*/
public function setPage(\Acme\PageBundle\Entity\Page $page)
{
$this->page = $page;
return $this;
}
User Entity:
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* The User create the Comment so he's supposed to be the owner of this relationship
* However, Doctrine doc says: "The many side of OneToMany/ManyToOne bidirectional relationships must be the owning
* side", so Comment is the owner
*
* One User can write Many Comments
*
* #ORM\OneToMany(targetEntity="Acme\CommentBundle\Entity\Comment", mappedBy="user")
*/
protected $comments;
...
/**
* Get Comments
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments() {
return $this->comments ?: $this->comments = new ArrayCollection();
}
Page Entity:
/**
* #ORM\Entity
* #ORM\Table(name="page")
*/
class Page
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* One Page can have Many Comments
* Owner is Comment
*
* #ORM\OneToMany(targetEntity="\Acme\CommentBundle\Entity\Comment", mappedBy="page")
*/
protected $comments;
...
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments(){
return $this->comments ?: $this->comments = new ArrayCollection();
}
I want a bidirectional relationship to be able to get the collection of Comments from the Page or from the User (using getComments()).
My problem is that when I try to save a new Comment, I get an error saying that doctrine is not able to create a Page entity. I guess this is happening because it's not finding the Page (but it should) so it's trying to create a new Page entity to later link it to the Comment entity that I'm trying to create.
Here is the method from my controller to create a Comment:
public function createAction()
{
$user = $this->getUser();
$page = $this->getPage();
$comment = new EntityComment();
$form = $this->createForm(new CommentType(), $comment);
if ($this->getRequest()->getMethod() === 'POST') {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$comment->setPage($page);
$comment->setUser($user);
$em->persist($comment);
$em->flush();
return $this->redirect($this->generateUrl('acme_comment_listing'));
}
}
return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
'form' => $form->createView()
));
}
I don't understand why this is happening. I've checked my Page object in this controller (returned by $this->getPage() - which return the object stored in session) and it's a valid Page entity that exists (I've checked in the DB too).
I don't know what to do now and I can't find anyone having the same problem :(
This is the exact error message I have:
A new entity was found through the relationship
'Acme\CommentBundle\Entity\Comment#page' that was not configured to
cascade persist operations for entity:
Acme\PageBundle\Entity\Page#000000005d8a1f2000000000753399d4. 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"}). If you cannot
find out which entity causes the problem implement
'Acme\PageBundle\Entity\Page#__toString()' to get a clue.
But I don't want to add cascade={"persist"} because I don't want to create the page on cascade, but just link the existing one.
UPDATE1:
If I fetch the page before to set it, it's working. But I still don't know why I should.
public function createAction()
{
$user = $this->getUser();
$page = $this->getPage();
// Fetch the page from the repository
$page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
'id' => $page->getId()
));
$comment = new EntityComment();
// Set the relation ManyToOne
$comment->setPage($page);
$comment->setUser($user);
$form = $this->createForm(new CommentType(), $comment);
if ($this->getRequest()->getMethod() === 'POST') {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirect($this->generateUrl('acme_comment_listing'));
}
}
return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
'form' => $form->createView()
));
}
UPDATE2:
I've ended up storing the page_id in the session (instead of the full object) which I think is a better idea considering the fact that I won't have a use session to store but just the id. I'm also expecting Doctrine to cache the query when retrieving the Page Entity.
But can someone explain why I could not use the Page entity from the session? This is how I was setting the session:
$pages = $site->getPages(); // return doctrine collection
if (!$pages->isEmpty()) {
// Set the first page of the collection in session
$session = $request->getSession();
$session->set('page', $pages->first());
}
Actually, your Page object is not known by the entity manager, the object come from the session. (The correct term is "detached" from the entity manager.)
That's why it tries to create a new one.
When you get an object from different source, you have to use merge function. (from the session, from an unserialize function, etc...)
Instead of
// Fetch the page from the repository
$page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
'id' => $page->getId()
));
You can simply use :
$page = $em->merge($page);
It will help you if you want to work with object in your session.
More information on merging entities here

Resources