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;
}
Related
I have a command which take a long time to run (it generates a big file).
I would like to use a controller to start it in background and don't wait for the end of its execution to render a view.
Is it possible? If yes, how?
I though the Process class would be useful but the documentation says:
If a Response is sent before a child process had a chance to complete, the server process will be killed (depending on your OS). It means that your task will be stopped right away. Running an asynchronous process is not the same as running a process that survives its parent process.
I solved my problem using the Messenger component as #msg suggested in comments.
To do so, I had to:
install the Messenger component by doing composer require symfony/messenger
create a custom log entity to track the file generation
create a custom Message and a custom MessageHandler for my file generation
dispatch the Message in my controller view
move my command code to a service method
call the service method in my MessageHandler
run bin/console messenger:consume -vv to handle the messages
Here is my code:
Custom log entity
I use it to show in my views if a file is being generated and to let the user download the file if its generation is complete
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\MyLogForTheBigFileRepository")
*/
class MyLogForTheBigFile
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $generationDateStart;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $generationDateEnd;
/**
* #ORM\Column(type="string", length=200, nullable=true)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $generator;
public function __construct() { }
// getters and setters for the attributes
// ...
// ...
}
Controller
I get the form submission and dispatch a message which will run the file generation
/**
* #return views
* #param Request $request The request.
* #Route("/generate/big-file", name="generate_big_file")
*/
public function generateBigFileAction(
Request $request,
MessageBusInterface $messageBus,
MyFileService $myFileService
)
{
// Entity manager
$em = $this->getDoctrine()->getManager();
// Creating an empty Form Data Object
$myFormOptionsFDO = new MyFormOptionsFDO();
// Form creation
$myForm = $this->createForm(
MyFormType::class,
$myFormOptionsFDO
);
$myForm->handleRequest($request);
// Submit
if ($myForm->isSubmitted() && $myForm->isValid())
{
$myOption = $myFormOptionsFDO->getOption();
// Creating the database log using a custom entity
$myFileGenerationDate = new \DateTime();
$myLogForTheBigFile = new MyLogForTheBigFile();
$myLogForTheBigFile->setGenerationDateStart($myFileGenerationDate);
$myLogForTheBigFile->setGenerator($this->getUser());
$myLogForTheBigFile->setOption($myOption);
// Save that the file is being generated using the custom entity
$em->persist($myLogForTheBigFile);
$em->flush();
$messageBus->dispatch(
new GenerateBigFileMessage(
$myLogForTheBigFile->getId(),
$this->getUser()->getId()
));
$this->addFlash(
'success', 'Big file generation started...'
);
return $this->redirectToRoute('bigfiles_list');
}
return $this->render('Files/generate-big-file.html.twig', [
'form' => $myForm->createView(),
]);
}
Message
Used to pass data to the service
namespace App\Message;
class GenerateBigFileMessage
{
private $myLogForTheBigFileId;
private $userId;
public function __construct(int $myLogForTheBigFileId, int $userId)
{
$this->myLogForTheBigFileId = $myLogForTheBigFileId;
$this->userId = $userId;
}
public function getMyLogForTheBigFileId(): int
{
return $this->myLogForTheBigFileId;
}
public function getUserId(): int
{
return $this->userId;
}
}
Message handler
Handle the message and run the service
namespace App\MessageHandler;
use App\Service\MyFileService;
use App\Message\GenerateBigFileMessage;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class GenerateBigFileMessageHandler implements MessageHandlerInterface
{
private $myFileService;
public function __construct(MyFileService $myFileService)
{
$this->myFileService = $myFileService;
}
public function __invoke(GenerateBigFileMessage $generateBigFileMessage)
{
$myLogForTheBigFileId = $generateBigFileMessage->getMyLogForTheBigFileId();
$userId = $generateBigFileMessage->getUserId();
$this->myFileService->generateBigFile($myLogForTheBigFileId, $userId);
}
}
Service
Generate the big file and update the logger
public function generateBigFile($myLogForTheBigFileId, $userId)
{
// Get the user asking for the generation
$user = $this->em->getRepository(User::class)->find($userId);
// Get the log object corresponding to this generation
$myLogForTheBigFile = $this->em->getRepository(MyLogForTheBigFile::class)->find($myLogForTheBigFileId);
$myOption = $myLogForTheBigFile->getOption();
// Generate the file
$fullFilename = 'my_file.pdf';
// ...
// ...
// Update the log
$myLogForTheBigFile->setGenerationDateEnd(new \DateTime());
$myLogForTheBigFile->setFilename($fullFilename);
$this->em->persist($myLogForTheBigFile);
$this->em->flush();
}
I'm trying to dispatch a custom event from the Rabbitmq Consumer. This process used to work on Symfony 3.4, recently upgraded the project to Symfony Flex(4.3).
DatasetSubmissionConsumer.php
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
/**
* A consumer of dataset submission messages.
*
* #see ConsumerInterface
*/
class DatasetSubmissionConsumer implements ConsumerInterface
{
/**
* The entity event dispatcher.
*
* #var EntityEventDispatcher
*/
protected $entityEventDispatcher;
/**
* Constructor.
*
* #param EntityEventDispatcher $entityEventDispatcher The entity event dispatcher.
*/
public function __construct(
EntityEventDispatcher $entityEventDispatcher,
) {
$this->entityEventDispatcher = $entityEventDispatcher;
}
/**
* Process a filer message.
*
* #param AMQPMessage $message A filer message.
*
* #return boolean True if success, false otherwise.
*/
public function execute(AMQPMessage $message)
{
$datasetSubmissionId = $message->body;
// Do Something //
$this->entityEventDispatcher->dispatch($datasetSubmission, 'dataset_processed');
return true;
}
}
DatasetSubmissionListener.php
/**
* Listener class for Dataset Submission-related events.
*/
class DatasetSubmissionListener
{
/**
* Method to send an email to DRPM on a dataset_processed event.
*
* #param EntityEvent $event Event being acted upon.
*
* #return void
*/
public function onDatasetProcessed(EntityEvent $event)
{
$datasetSubmission = $event->getEntity();
// Added if-statement so that emails are sent to data-managers only
// Do Something and send Email
}
}
EntityEventDispatcher.php
<?php
namespace App\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* An entity event dispatcher.
*/
class EntityEventDispatcher
{
/**
* The event dispatcher to use in this entity event dispatcher.
*
* #var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* Constructor.
*
* #param EventDispatcherInterface $eventDispatcher The event dispatcher to use.
*/
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* Dispatch an Entity event.
*
* #param Entity $entity The Entity the event is for.
* #param string $entityEventName The name of the entity event.
*
* #return void
*/
public function dispatch(Entity $entity, string $entityEventName)
{
$this->eventDispatcher->dispatch(
'pelagos.entity.' . $entity->getUnderscoredName() . '.' . $entityEventName,
new EntityEvent($entity)
);
}
}
App\Event\DatasetSubmissionListener:
tags:
- { name: kernel.event_listener, event: pelagos.entity.dataset_submission.dataset_processed, method: onDatasetProcessed }
Also ran the bin/console debug:event-dispatcher and the event is callable
"pelagos.entity.dataset_submission.dataset_processed" event
-----------------------------------------------------------
------- ----------------------------------------------------------- ----------
Order Callable Priority
------- ----------------------------------------------------------- ----------
#1 App\Event\DatasetSubmissionListener::onDatasetProcessed() 0
------- ----------------------------------------------------------- ----------
Using Symfony 4.3, Rabbitmq 3.3.5
The Event Listener doesn't catch this dispatched event after the Rabbitmq Consumer exits with a success. Is there a way to debug or make this work?
Thanks.
I figured out that we can dispatch the event from Rabbitmq Consumer. The issue was with the SwiftMailer not sending emails from the dispatched event in the Event Listener.
DatasetSubmissionListener.php
/**
* Listener class for Dataset Submission-related events.
*/
class DatasetSubmissionListener
{
/**
* Method to send an email to DRPM on a dataset_processed event.
*
* #param EntityEvent $event Event being acted upon.
*
* #return void
*/
public function onDatasetProcessed(EntityEvent $event)
{
$datasetSubmission = $event->getEntity();
// Added if-statement so that emails are sent to data-managers only
// Do Something and send Email
sendEmailMessage($this->twig->load('Email/data-repository-managers.dataset-processed.email.twig'), $this->tokenStorage->getToken()->getUser());
}
/**
* Method to build and send an email.
*
* #param \Twig\TemplateWrapper $emailTwigTemplate A twig template.
* #param array $mailData Mail data array for email.
* #param array $toAddresses Recipient's email addresses.
* #param array $attachments An optional array of Swift_Message_Attachments to attach.
*
* #throws \InvalidArgumentException When any element of $attachments is not a Swift_Message_Attachment.
*
* #return void
*/
public function sendEmailMessage(
\Twig\TemplateWrapper $emailTwigTemplate,
array $mailData,
array $toAddresses = array(),
array $attachments = array()
) {
$message = new \Swift_Message();
$message
->setSubject($emailTwigTemplate->renderBlock('subject', $mailData))
->setFrom($this->from)
->setTo($toAddresses)
->setBody($emailTwigTemplate->renderBlock('body_html', $mailData), 'text/html')
->addPart($emailTwigTemplate->renderBlock('body_text', $mailData), 'text/plain');
foreach ($attachments as $attachment) {
if (!$attachment instanceof \Swift_Attachment) {
throw new \InvalidArgumentException('Attachment is not an instance of Swift_Attachment');
}
$message->attach($attachment);
}
$this->mailer->send($message);
}
}
The config which I had for swiftmailer previously:
swiftmailer:
url: '%env(MAILER_URL)%'
transport: sendmail
spool: { type: memory }
As I was having the spool feature enabled, the swiftmailer was waiting
for the kernel terminate event from the Rabbitmq Consumers, and the
consumers don't get terminated after acknowledging a message
To fix the issue, all I did was remove the spool feature.
swiftmailer:
url: '%env(MAILER_URL)%'
transport: sendmail
I have SMS notifications that get sent via Nexmo. They are working, but I've changed how I set the recipient's phone number and need to override the default by using the routeNotificationForNexmo method.
However, it seems no matter what I do with that method, the app ignores it and nothing changes. I can change other things in this file, such as the SMS message contents, and see the changes in new messages sent from Nexmo.
Why isn't routeNotificationForNexmo working? Is there something else that could be overriding this?
Here is my whole /Notifications/TeamMessage.php file:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\NexmoMessage;
class TeamMessage extends Notification implements ShouldQueue
{
use Queueable;
protected $custom_message;
protected $short_code;
protected $reply_to;
protected $reply_to_name;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($custom_message, $short_code, $channels, $reply_to, $reply_to_name)
{
$this->custom_message = $custom_message;
$this->short_code = $short_code;
$this->channels = $channels;
$this->reply_to = $reply_to;
$this->reply_to_name = $reply_to_name;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return $this->channels; //
}
/**
* Route notifications for the Nexmo channel.
*
* #return string
*/
public function routeNotificationForNexmo()
{
$number = $this->phone_number_country . $this->phone_number;
$number = preg_replace("/[^0-9]/","", $number); //Strips all non-numbers
return $number;
}
/* Send SMS */
public function toNexmo($notifiable)
{
return (new NexmoMessage)
->from(00000000000)
->content($this->custom_message . " \n\n Unsubscribe at http://" . env('TL_DOMAIN') . '/s/' . $this->short_code );
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->replyTo($this->reply_to, $this->reply_to_name)
->line($this->custom_message)
->line('--')
->line("To unsubscribe, go to http://" . env('TL_DOMAIN') . '/s/' . $this->short_code);
//->action('Notification Action', 'https://laravel.com');
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}
I'm on Laravel 5.3, running locally via Docker (Laradock). I've tried stopping and restarting all the containers.
Solved. Not sure where I got the idea to put the routeNotificationForNexmo() function inside the notification class. It belongs in the the User class, User.php file.
I have 2 entities Submission and Documents. 1 Submission can have Multiple documents.
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $document;
/**
* #return mixed
*/
public function getDocument()
{
return $this->document->toArray();
}
public function setDocument(Document $document)
{
$this->document[] = $document;
return $this;
}
Document Entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="document")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id",onDelete="cascade", nullable=true)
*/
protected $submission;
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
After receiving files dropzonejs - I'm saving them into Document object, and then, i'm try to save this object into Submission, and persist.
$document = new Document();
$em = $this->getDoctrine()->getManager();
$media = $request->files->get('file');
foreach($media as $req){
$document->setFile($req);
$document->setPath($req->getPathName());
$document->setName($req->getClientOriginalName());
$em->persist($document);
}
$submission->setSubmissionStatus(true);
foreach($document as $item){
$submission->setDocument($item);
}
$submission->setUser($user);
$em = $this->getDoctrine()->getManager();
$em->persist($submission);
$em->flush();
Problem is that all the time, i'm receiving error that submission_title is not set, but that's not true, because i have set this field before. I haven't got idea, what is wrong.
I think you'll get some mileage out of following the tutorial over at http://symfony.com/doc/current/doctrine/associations.html, if you haven't already.
I can see that your getters / setters aren't optimal for associating more than one Document with your Submission.
As they write in the Symfony docs, where they want to associate one category with many products, they have the following code:
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
}
From the docs:
The code in the constructor is important. Rather than being
instantiated as a traditional array, the $products property must be of
a type that implements Doctrine's Collection interface. In this case,
an ArrayCollection object is used. This object looks and acts almost
exactly like an array, but has some added flexibility. If this makes
you uncomfortable, don't worry. Just imagine that it's an array and
you'll be in good shape.
So, you'll want to be sure the constructor for your Document entity has something like $this->submissions = new ArrayCollection();. I've changed the property to a plural name, because I think it's more semantically correct. But you can keep your $submission property name, if you like.
Next is to add a addSubmission, removeSubmission, and a getSubmissions method.
Then, your class might end up looking like this:
<?php
// src/AppBundle/Entity/Submission.php
namespace AppBundle\Entity
use Doctrine\Common\Collections\ArrayCollection;
class Submission
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*
* #var ArrayCollection()
*/
protected $documents;
...
/**
* Instantiates the Submission Entity
*
* #return void
*/
public function __construct()
{
$this->documents = new ArrayCollection();
}
/**
* Returns all documents on the Submission
*
* #return mixed
*/
public function getDocuments()
{
return $this->documents;
}
/**
* Add document to this Submission
*
* #param Document $document The object to add to the $documents collection.
*
* #return Submission
*/
public function setDocument(Document $document)
{
$this->documents[] = $document;
return $this;
}
/**
* Remove a document from this Submission
*
* #param Document $document The object to remove from the $documents collection.
*
* #return Submission
*/
public function removeDocument(Document $document)
{
$this->documents->removeElement($document);
return $this;
}
}
I'm writing a functional test for an Action entity having a relationship with the User entity:
<?php
namespace Acme\AppBundle\Entity;
/**
* Class Action
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Acme\AppBundle\Repository\ActionRepository")
*/
class Action
{
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Acme\AppBundle\Entity\User
*
* #ORM\ManyToOne(targetEntity="\Acme\AppBundle\Entity\User", inversedBy="actions")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $createdBy;
}
User:
namespace Acme\AppBundle\Entity;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Action", mappedBy="createdBy")
*/
private $actions;
}
And the user is setted in the controller with the following snippet:
<?php
namespace Acme\ApiBundle\Controller;
/**
*
* #Route("/actions")
*/
class ActionController extends FOSRestController
{
public function postAction(Request $request)
{
$action = new Action();
$action->setCreatedBy($this->getUser());
return $this->processForm($action, $request->request->all(), Request::METHOD_POST);
}
}
When calling the action with a REST client for example, everything works fine, the relationship between Action and User is persisted correctly.
Now, when testing the action with a functional test, the relationship is not working because of the following error:
A new entity was found through the relationship 'Acme\AppBundle\Entity\Action#createdBy' that was not configured to cascade persist operations for entity: test. 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"}).
For my functional test I need to inject a JWT and a session token because my routes are secured by a JWT and I need to have a user in session.
Here is how I inject that:
<?php
namespace Acme\ApiBundle\Tests;
class ApiWebTestCase extends WebTestCase
{
/**
* #var ReferenceRepository
*/
protected $fixturesRepo;
/**
* #var Client
*/
protected $authClient;
/**
* #var array
*/
private $fixtures = [];
protected function setUp()
{
$fixtures = array_merge([
'Acme\AppBundle\DataFixtures\ORM\LoadUserData'
], $this->fixtures);
$this->fixturesRepo = $this->loadFixtures($fixtures)->getReferenceRepository();
$this->authClient = $this->createAuthenticatedClient();
}
/**
* Create a client with a default Authorization header.
*
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->fixturesRepo->getReference('user-1');
$jwtManager = $this->getContainer()->get('lexik_jwt_authentication.jwt_manager');
$token = $jwtManager->create($user);
$this->loginAs($user, 'api');
$client = static::makeClient([], [
'AUTHENTICATION' => 'Bearer ' . $token,
'CONTENT_TYPE' => 'application/json'
]);
$client->disableReboot();
return $client;
}
}
Now, the issue is that the injected UsernamePasswordToken contains a User instance which is detached from the current EntityManager, thus resulting in the Doctrine error above.
I could merge the $user object in the postAction method into the EntityManager but I don't want to do that because it means I modify my working code to make a test passes.
I've also tried directling merging the $user object in my test into the EntityManager like this:
$em = $client->getContainer()->get('doctrine')->getManager();
$em->merge($user);
But it's not working either.
So now, I'm stuck, I really don't know what to do except that I need to attach the user in session back to the current EntityManager.
The error message you are getting indicates that the EntityManager contained in the test client's container doesn't know about your User entity. This leads me to believe that the way you are retrieving the User in your createAuthenticatedClient method is using a different EntityManager.
I suggest you try to use the test kernel's EntityManager to retrieve the User entity instead. You can get it from the test client's container, for example.
Thanks to your tweet, I come to complete the given answer and (try to) propose a solution,
The problem is that your user is not managed by the EntityManager, and more simply, because it's not a real existing user that is registered in database.
To get around this problem, you need to have a real (managed) user that doctrine could use for the association that your action is trying to create.
So, you can either create this user at each execution of your functional test case (and delete it when finished), or create it only once when execute the test case for the first time on a new environment.
Something like this should do the trick:
/** #var EntityManager */
private $em;
/**
*/
public function setUp()
{
$client = static::createClient();
$this->em = $client->getKernel()
->getContainer()
->get('doctrine');
$this->authClient = $this->createAuthenticatedClient();
}
/**
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->em
->getRepository('Acme\AppBundle\Entity\User')
->findOneBy([], ['id' => DESC]; // Fetch the last created
// ...
return $client;
}
That's a pity for your fixtures (that are so much sexier), but I don't see any way to attach your fixture as a real entry, as you can't interact more with the tested controller.
Another way would be to create a request to your login endpoint, but it would be even more ugly.