Trying to use notifier in an event Subscriber - symfony

I'm learnig the event Subscriber on Symfony, my idea is when I create a new blog post, I want to send a notification. I'm already able to get the last entity created.
I've added to code to get the Symfony notifier and be able to send a notification on Disocrd. I currently have the following code :
class EasyAdminSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
AfterEntityPersistedEvent::class => ['setBlogPostSlug'],
];
}
/**
* #param AfterEntityPersistedEvent $event
* #param ChatterInterface $chatter
* #return void
* #throws \Symfony\Component\Notifier\Exception\TransportExceptionInterface
*/
public function setBlogPostSlug(AfterEntityPersistedEvent $event)
{
$chatter = new Chatter();
$entity = $event->getEntityInstance();
if (!($entity instanceof Blog)) {
return;
}
$message = (new ChatMessage('You got a new invoice for 15 EUR.'));
// if not set explicitly, the message is send to the
// default transport (the first one configured)
$chatter->send($message);
}
}
But when I run the code I have the followinf error :
App\EventSubscriber\EasyAdminSubscriber::setBlogPostSlug(): Argument #2 ($chatter) must be of type Symfony\Component\Notifier\ChatterInterface, string given
I don't understand what's wrong. Do you any idea why ? And how to make things work ?

Related

Twilio Symfony - The controller must return a \"Symfony\\Component\\HttpFoundation\\Response\" but returned Twilio\\TwiML\\VoiceResponse."

I want to implement Twilio browser to browser call with Symfony5 and ApiPlatform
I'm following this tuto:
https://www.twilio.com/docs/voice/client/tutorials/calls-between-devices?code-sample=code-generate-twiml-from-client-parameters-3&code-language=PHP&code-sdk-version=5.x
I have this function, that's the one I want my TwiML app to be configured on
/**
* #Route("/twilio/handle/twiml/{clientId}", name="twilio_handl_twiml")
* #param $clientId
* #return VoiceResponse
*/
public function handleTwiml($clientId): VoiceResponse
{
/** #var Client $client */
$client = $this->clientRepository->findOneBy(['id' => 11]);
$to = $client->getUser()->getLastName().$client->getUser()->getId();
$voiceResponse = new VoiceResponse();
$number = htmlspecialchars($to);
$dial = $voiceResponse->dial(null, array('callerId' => '+15017122661'));
if (isset($to)) {
if (preg_match("/^[\d\+\-\(\) ]+$/", $number)) {
$dial->number($number);
} else {
$dial->client($number);
}
} else {
$voiceResponse->say('There has been an issue. Thanks for calling!');
}
return $voiceResponse;
}
And I've declared it as a custom route on one of my entities in the "get" section:
* "twilio_handl_twiml"={
* "path"="/twilio/handle/twiml/{clientId}",
* "controller"="TwilioController:class"
* },
Now the function creates a proper VoiceResponse object
But when I call this route I get the following error message:
The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned an object of type Twilio\TwiML\VoiceResponse.
Now does anyone know why I couldn't return whatever kind of Response I want from a custom route ?
I don't really see why the framework would declare this as an error
If anyone can help me understand better this error I'd appreciate it
Thanks!
Twilio developer evangelist here.
As #Cerad has said in the comments, you need to respond with an object derived from the Symfony Response object.
I haven't used Symfony, so please excuse me if this is wrong, but I think you can update your handler to the following, it might work:
use Symfony\Component\HttpFoundation\Response;
/**
* #Route("/twilio/handle/twiml/{clientId}", name="twilio_handl_twiml")
* #param $clientId
* #return Response
*/
public function handleTwiml($clientId): VoiceResponse
{
/** #var Client $client */
$client = $this->clientRepository->findOneBy(['id' => 11]);
$to = $client->getUser()->getLastName().$client->getUser()->getId();
$voiceResponse = new VoiceResponse();
$number = htmlspecialchars($to);
$dial = $voiceResponse->dial(null, array('callerId' => '+15017122661'));
if (isset($to)) {
if (preg_match("/^[\d\+\-\(\) ]+$/", $number)) {
$dial->number($number);
} else {
$dial->client($number);
}
} else {
$voiceResponse->say('There has been an issue. Thanks for calling!');
}
$response = new Response(
$voiceResponse->asXML(),
Response::HTTP_OK,
['content-type' => 'application/xml']
);
return $response;
}
The key here is to build up the Symfony response with the content of the voice response ($voiceResponse->asXML()) and also set the content type to application/xml.

How to execute a Symfony command in background from a controller

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();
}

How to rebuild local action

I would like to add one action to a page (let's say the route entity.node.canonical) but this action would appear and disappear from the page time to time.
So what I'm trying to do is to create the action using a deriver (I put my condition to show the action in the method getDerivativeDefinitions) and then, on a kernel event, I refresh the local actions using \Drupal::service('plugin.manager.menu.local_action')->clearCachedDefinitions();
But it still doesn't work !
So is anybody capable of showing me a method to show and hide an action ?
Here is my derivative:
class ConditionalAction extends DeriverBase implements ContainerDeriverInterface {
/**
* {#inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition)
{
// If the seconds is more than 30, hide the action.
if (date('s') > 30) {
return $this->derivatives;
}
// Else, show the action.
$menu_entry = $base_plugin_definition;
$menu_entry['route_name'] = 'my.route.name';
$menu_entry['title'] = 'Test action';
$menu_entry['appears_on'][] = \Drupal::routeMatch()->getRouteName();
$menu_entry['route_parameters'] = ['time' => date('s')];
$this->derivatives['my.action.name'] = $menu_entry;
return $this->derivatives;
}
}
And here is the event subscriber:
class MyModuleKernelSubscriber implements EventSubscriberInterface {
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
return [
KernelEvents::REQUEST => 'onKernelRequest'
];
}
/**
* Event called when a request is sent.
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event) {
// Do not consider the ajax requests.
$request = $event->getRequest();
if ($request->isXmlHttpRequest() === TRUE) {
return;
}
// Flush local action cache.
\Drupal::service('plugin.manager.menu.local_action')->clearCachedDefinitions();
}
}
So, here, on each request, the local action should be cleared and the code should go through my derivative. But it's not working... Any thought ?
I really need it to be an action ! And I really need the URL argument to be dynamic as well (here, $menu_entry['route_parameters'] = ['time' => date('s')];).
Thank you
By looking inside the console command drupal cache:reset, I found that the lines
$bins = Cache::getBins();
$bins['render']->deleteAll();
$bins['discovery']->deleteAll();
reset the local actions on each requests.
So I have my answer now.
Hope this will help !

Vich UploadedFile, FOSUserBundle and Events

I added a VichImageType field on edit profile twig template...so im trying to check image dimensions using vich_uploader.pre_upload as Event.
In my class i got the image properties and if their dimensions arent bigger enough i tried to stop propagation and flashed a message to the twig template but, i dont know why, the event keeps propagating and redirects to fos_user_profile_show showing the image setup. Also, i tried to redirect again to fos_user_profile_edit but i cant use $event because "Vich\UploaderBundle\Event\Event" doesnt implement setController(). How can achieve this?
This is the method of the Listener class:
namespace BackendBundle\EventListener;
use Vich\UploaderBundle\Event\Event;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
class ComprobarDimensionesDeImagen
{
private $requestStack;
private $router;
public function __construct(RequestStack $requestStack, RouterInterface $router)
{
$this->requestStack = $requestStack;
$this->router = $router;
}
public function onVichUploaderPreUpload(Event $event)
{
$object = $event->getObject();
$mapping = $event->getMapping();
$imagen = getimagesize($object->getImageFile());
if (250 > $imagen[0] || 250 > imagen[1]) {
$request = $this->requestStack->getCurrentRequest();
$session = $request->getSession();
$event->stopPropagation();
$session->getFlashBag()->add('error', "Minimum dimensions: 250x250 \n");
$url = $this->router->generate('fos_user_profile_edit');
/*
* Testing different methods of redirect
*
* $response = new RedirectResponse($url);
* $event->setResponse($response);
*/
$event->setController(function() use ($request) {
return new RedirectResponse($url);
});
}
}
}
When i edit the profile again, I can see the flash message and the image setup in VichImageType field (i didnt expect that stopping propagation). Any help will be very welcome.
SOLVED: Just using #Assert\Image in my Entity class did the validation. No service neither listener needed
The argument for ->setController must be a callable . In your case, the function you pass as an argument returns an object of type Response. In order to be callable, the method should have the suffix Action. See also this post.

am new with symfony and i need some advices

I have a problem regarding a Symfony application, I want to take as input the "username" or "id" for my controller , and receive information that is in my table "user" and also 2 other table for example : A user has one or more levels , and also it has points must earn points to unlock a level , I want my dan Home page display the username and the level and extent that it has , I jn am beginner and not come to understand the books symfony that I use, I work with PARALLEL " symfony_book " and " symfony_cook_book " and also tutorial youtube May I blocks , here is the code for my cotroler
"
/**
* #Route("/{id}")
* #Template()
* #param $id=0
* #return array
*/
public function getUserAction($id)
{
$username = $this->getDoctrine()
->getRepository('voltaireGeneralBundle:FosUser')
->find($id);
if (!$username) {
throw $this->createNotFoundException('No user found for id '.$id);
}
//return ['id' => $id,'username' => $username];
return array('username' => $username);
}
and I have to use the relationship among classes
use Doctrine\Common\Collections\ArrayCollection;
class Experience {
/**
* #ORM\OneToMany(targetEntity="FosUser", mappedBy="experience")
*/
protected $fosUsers;
public function __construct()
{
$this->fosUsers = new ArrayCollection();
}
}
and
class FosUser {
/**
* #ORM\ManyToOne(targetEntity="Experience", inversedBy="fosUsers")
* #ORM\JoinColumn(name="experience_id", referencedColumnName="id")
*/
protected $fosUsers;
}
and i have always an error
In Symfony you cannot return an array in Action function!, Action function must always return a Response object...So if you want to return data to browser in Symfony, Action function have to return a string wrapped up in Response object.
In your controller code, to return the array to browser, You can serialize an array to JSON and send it back to browser:
public function getUserAction($id)
{
$username = $this->getDoctrine()
->getRepository('voltaireGeneralBundle:FosUser')
->find($id);
if (!$username) {
throw $this->createNotFoundException('No user found for id '.$id);
}
return new Response(json_encode(array('username' => $username)));
}
I suggest you to read more about HTTP protocol , PHP, and Symfony.

Resources