I'm trying to create a controller action that results in two responses. The SwiftMailer uses the kernel.terminate event to accomplish this. I can build an event listener for the event but I don't know how to tell it what I want it to do. I know what it takes to create and download a pdf file, but how will the listener know when to do it?
Edit:
The "tell it" is found here in SO by doing this:
if ($nextAction) {
$request->attributes->set('household_id', $id);
}
But it is not at all clear how to get the event listener to do all of this (copied from a controller, but for the first line):
$em = $this->container->get('doctrine.orm.default_entity_manager');
$household = $em->getRepository('ManaClientBundle:Household')->find($id);
$fname = $household->getHead()->getFname();
$sname = $household->getHead()->getSname();
$filename = $sname . $fname . 'card.pdf';
$stylesheetXml = $this->renderView('ManaClientBundle:Test:pdfstyle.xml.twig', array());
$facade = $this->get('ps_pdf.facade');
$response = new Response();
$this->render('ManaClientBundle:Test:card.pdf.twig', array(
'household' => $household,
'date' => date_create(),
), $response);
$xml = $response->getContent();
$content = $facade->render($xml, $stylesheetXml);
header('content-type:application/pdf;' .
'Content-Disposition:attachment; filename=' . $filename);
echo $content;
Form includes:
->add('save', 'submit')
->add('saveCreate', 'submit')
Controller includes:
$nextAction = $form->get('saveCreate')->isClicked();
if ($nextAction) {
// tell event to create and download a pdf file using $id
}
return $this->redirect($this->generateUrl('household_show', array('id' => $id)));
service:
listener.pdfresponse:
class: Mana\ClientBundle\EventListener\PdfListenerSubscriber
arguments: [ #service_container ]
tags:
- { name: kernel.event_listener, event: kernel.terminate, method: onKernelTerminate }
Listener
namespace Mana\ClientBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PdfListenerSubscriber implements EventSubscriberInterface {
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function onKernelTerminate(PostResponseEvent $event) {
//create and download pdf file
}
static public function getSubscribedEvents()
{
return array(
KernelEvents::TERMINATE => 'onKernelTerminate');
}
}
Two responses to an HTTP request would never be possible - once the first request has been responded to, the connection is dropped by the web server so there would be nowhere for the second response to be sent.
Related
In a Laravel project (Laravel 8 on PHP 8.0) I have a feature test in which I test an internal endpoint. The endpoint has a Controller calls a method on a Service. The Service then tries to call a third-party endpoint. It is this third-party endpoint that I would like to mock. The situation currently looks like this:
Internal Endpoint Feature Test
public function testStoreInternalEndpointSuccessful(): void
{
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
Internal Endpoint Controller
class InternalEndpointController extends Controller
{
public function __construct(protected InternalService $internalService)
{
}
public function store(Request $request): InternalResource
{
$data = $this.internalService->fetchExternalData();
return new InternalResource($data); // etc.
}
}
Internal Service
use GuzzleHttp\ClientInterface;
class InternalService
{
public function __construct(protected ClientInterface $client)
{
}
public function fetchExternalData()
{
$response = $this->httpClient->request('GET', 'v1/external-data');
$body = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);
return $body;
}
}
I have looked at Guzzle's documentation, but it seems like the MockHandler strategy requires you to execute the http request inside of the test, which is not wat I want in my test. I want Guzzle's http client to be mocked and to return a custom http response that I can specify in my test. I have tried to mock Guzzle's http client like this:
public function testStoreInternalEndpointSuccessful(): void
{
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
$mock = new MockHandler([
new GuzzleResponse(200, [], $contactResponse),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$mock = Mockery::mock(Client::class);
$mock
->shouldReceive('create')
->andReturn($client);
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
But the InternalService does not seem to hit this mock in the test.
I have also considered and tried to use Http Fake, but it didn't work and I assume Guzzle's http client does not extend Laravel's http client.
What would be the best way to approach this problem and mock the third-party endpoint?
Edit
Inspired by this StackOverflow question, I have managed to solve this problem by injecting a Guzzle client with mocked responses into my service. The difference to the aforementioned StackOverflow question is that I had to use $this->app->singleton instead of $this->app->bind because my DI was configured differently:
AppServiceProvider.php
namespace App\Providers;
use App\Service\InternalService;
use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// my app uses ->singleton instead of ->bind
$this->app->singleton(InternalService::class, function () {
return new InternalService(new Client([
'base_uri' => config('app.internal.base_url'),
]));
});
}
}
Depending on your depending injection, you want to bind or singleton-ify your InternalService with a custom Guzzle http client that returns mocked responses, e.g. like this:
public function testStoreInternalEndpointSuccessful(): void
{
// depending on your DI configuration,
// this could be ->bind or ->singleton
$this->app->singleton(InternalService::class, function($app) {
$mockResponse = json_encode([
'data' => [
'id' => 0,
'name' => 'Jane Doe',
'type' => 'External',
'description' => 'Etc. you know the drill',
]
]);
$mock = new GuzzleHttp\Handler\MockHandler([
new GuzzleHttp\Psr7\Response(200, [], $mockResponse),
]);
$handlerStack = GuzzleHttp\HandlerStack::create($mock);
$client = new GuzzleHttp\Client(['handler' => $handlerStack]);
return new InternalService($client);
});
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
See also: Unit Testing Guzzle inside of Laravel Controller with PHPUnit
I am trying to build my first no-framework PHP application and I am following this tutorial.
I am relatively new to some concepts described in the tutorial. Despite this, I decided to use, as Dependency Injector, PHP-DI instead of the suggested one (rdlowrey/auryn).
I have created everything according to the tutorial except for the file Bootstrap.php (and the file Dependencies.php:
<?php declare(strict_types = 1);
require(__DIR__ . '/../vendor/autoload.php');
...
$container = include('Dependencies.php');
$request = $container->make('Http\HttpRequest');
$response = $container->make('Http\HttpResponse');
...
switch ($routeInfo[0]) {
...
case \FastRoute\Dispatcher::FOUND:
$className = $routeInfo[1][0];
$method = $routeInfo[1][1];
$vars = $routeInfo[2];
$class = $container->make($className);
$class->$method($vars); // (**)
break;
}
echo $response->getContent(); // (*)
$class can be only an instance of a Homepage class which has only one method (show()), called in (**):
class Homepage
{
private $request;
private $response;
private $renderer;
public function __construct(
Request $request,
Response $response,
Renderer $renderer
) {
$this->request = $request;
$this->response = $response;
$this->renderer = $renderer;
}
public function show() {
$data = [
'name' => $this->request->getParameter('name', 'stranger'),
];
$html = $this->renderer->render('Homepage', $data);
$this->response->setContent($html); // (***)
}
}
With all that said, the application returns a 200 HTTP response with an empty body [here (*)]
but if I try to print the content of the HTTP response after (***) I get the correct response.
This could mean that there are two different instances of an HttpResponse class. (Is that right?)
By using rdlowrey/auryn, the author of the tutorial, used the method share() to share the same HttpReponse instance among classes, as shown in the "original" Dependencies.php file:
<?php declare(strict_types = 1);
use \Auryn\Injector;
...
$injector = new Injector;
$injector->alias('Http\Response', 'Http\HttpResponse');
$injector->share('Http\HttpResponse');
...
return $injector;
Is there a way to get the same behavior using PHP-DI (with PHP definitions)?
Here's my version of Dependencies.php:
<?php declare(strict_types = 1);
$definitions = [
'Http\Request' => DI\create('Http\HttpRequest')->constructor(
$_GET, $_POST, $_COOKIE, $_FILES, $_SERVER),
'Http\HttpRequest' => function () {
$r = new Http\HttpRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
return $r;
},
'Http\Response' => DI\create('Http\HttpResponse'),
'Twig\Environment' => function () {
$loader = new Twig\Loader\FilesystemLoader(
dirname(__DIR__) . '/templates');
$twig = new Twig\Environment($loader);
return $twig;
},
'Example\Template\TwigRenderer' => function (Twig\Environment $renderer) {
return new Example\Template\TwigRenderer($renderer);
},
'Example\Template\Renderer' => DI\create(
'Example\Template\TwigRenderer')->constructor(
DI\get('Twig\Environment')),
];
$containerBuilder = new DI\ContainerBuilder;
$containerBuilder->addDefinitions($definitions);
$container = $containerBuilder->build();
return $container;
In Bootstrap.php, getting (get()) HttpRequest/HttpResponse instances, instead of making (make()) them, solved the problem.
...
$container = include('Dependencies.php');
$request = $container->get('Http\HttpRequest');
$response = $container->get('Http\HttpResponse');
...
As clearly stated in the documentation:
The make() method works like get() except it will resolve the entry
every time it is called. [..] if the entry is an object, an new instance will be created every time [..]
First, i apologize for my bad english.
I'm here because i'm looking for some way to listen exception guzzle event to redirect to login page if i receive status code 401.
I found an event "PostTransactionEvent" allowing to get data struct of my response. It makes his job but i can't redirect to login page. It seems RedirectResponse method was not executed.
services.yml :
glpi.expire_listener:
class: GlpiBundle\Expire\ExpireListener
arguments: ["#router","#request_stack"]
tags:
- { name: kernel.event_listener, event: guzzle_bundle.post_transaction, method: check }
ExpireListener.php
namespace GlpiBundle\Expire;
use EightPoints\Bundle\GuzzleBundle\Events\GuzzleEventListenerInterface;
use EightPoints\Bundle\GuzzleBundle\Events\PostTransactionEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class ExpireListener implements GuzzleEventListenerInterface
{
protected $service_name;
protected $request_stack;
public function __construct($router,$request_stack)
{
$this->router = $router;
$this->request_stack = $request_stack;
}
public function check(PostTransactionEvent $event)
{
$response_transaction = $event->getTransaction();
$e = new ExpireApi();
$available = $e->deconnect($response_transaction);
if ($available) {
return new RedirectResponse($this->router->generate('logout'));
}
$event->setTransaction($response_transaction);
}
public function setServiceName($serviceName){
$this->service_name = $serviceName;
}
}
request :
$reponse_categories =$client->get('/apirest.php/itilcategory?searchText[itilcategories_id]='.self::ID_CAMPUS_ID,
[
"headers"=>
[
"App-Token"=>TOKEN,
"Session-Token"=>SESSION
],
'exceptions'=>false
]);
Thanks in advance for your help,
Use this code:
// redirect to login page
$redResponse = new RedirectResponse( $this->router->generate('login') );
$redResponse->send();
Here is the full code:
https://github.com/viher3/yuwik-frontend/blob/develop/src/EventListeners/Guzzle/ExpiredJwt.php
Regards!
I'm turning to this forum because I can't find a valid solution to my problem.
I have taken over the management of a Symfony2 application which processes orders, invoices... inside a company and the problem is that there isn't archiving functions on it. So, the manager asked me to add archiving 'by year' functionalities to the application (simply display data depending on a chosen year).
So, I decided to prefix all application routes by /{year}/, parameter which will match the year the manager want to see and, as all the documents are dated, I just have to update Doctrine requests for picking those that match the chosen year. So far no problems.
routes.yml
mes_routes:
resource: "mes_routes.yml"
prefix: /{year}
defaults: {'year': %current_year%}
With this, I have created a Symfony Extension which fills the 'current_year' var by default in my route definition, with the actual year if no year is provided.
MyAppExtension.php
class MyAppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Fill parameter with the current year
$container->setParameter('current_year', date("Y"));
}
}
Next, I have created a RouteListener that stores a previous route and its parameters inside a session var, when a user displays a new page (in order to display a same page but with a different year next)
LastRouteListener.php
class LastRouteListener
{
public function onKernelRequest(GetResponseEvent $event)
{
// Don't store subrequests
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$routeName = $request->get('_route');
$routeParams = $request->get('_route_params');
if ($routeName[0] == "_") {
return;
}
$routeData = ['name' => $routeName, 'params' => $routeParams];
// Don't store the same route twice
$thisRoute = $session->get('this_route', []);
if ($thisRoute == $routeData) {
return;
}
$session->set('last_route', $thisRoute);
$session->set('this_route', $routeData);
}
}
services.yml
myapp.last_route_event_listener:
class: MyApp\EventListener\LastRouteListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }
And finally, I have added a new controller which, via a dropdown menu in the application navbar, displays the current page the user is viewing, but with a different year
ArchiveController.php
class ArchiveController extends Controller
{
public function switchYearAction(Request $request, $year)
{
$session = $request->getSession();
$lastRoute = $session->get('last_route');
$route = $lastRoute["name"];
$routeParams = $lastRoute["params"];
if (array_key_exists("year", $routeParams)) {
$routeParams["year"] = $year;
$session->set("current_year", $year);
}
return $this->redirect($this->generateUrl($route, $routeParams));
}
}
Arrived here, everything work. If a user chose an other date, the application will display the same page but with the new date chosen.
However, and there is my problem, if, from a previous year, the user clicks on a link in the page, we come back to the actual year. Quite normal, because Twig paths in the application doesn't fill the 'year' routing parameter, and the router provide the current year by default.
So, my question is : How can I keep the chosen year in memory, and use it as a route parameter ?
First, I had thought about setting the local var 'current_year' when the application uses the switchYearAction(), but Symfony returns an error ('Frozen variable')
Next, I had thought about using a session var to store the chosen year, but I can't access to the session within my extension MyAppExtension.
There might be a third solution which consists in update all Twig paths and Controller redirect(), but it represents some much line to edit...
Or maybe with a routeEventListener... but I don't know how to proceed.
Thanks you in advance.
You can access the application session in Twig using {{ app.session }}. So something like this is possible:
{{ url('myapp.whatever', {year: app.session.get('current_year')}) }}
Since you have a bit of logic around your current year stuff (if it's not set in the session fallback to the current year, etc), a twig extension that provides a funtion to fetch the current year may be a better way to go. Quick, untested example:
<?php
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class CurrentYearExtension extends \Twig_Extension
{
private $session;
private $defaultYear;
public function __construct(SessionInterface $session, $defaultYear)
{
$this->session = $session;
$this->defaultYear = $defaultYear;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('current_year', [$this, 'currentYear']),
];
}
public function currentYear()
{
return $this->session->get('current_year') ?: $this->defaultYear;
}
}
Then add it to your container and tag it with the twig.extension tag.
<service id="myapp.currentyear_extension" class="CurrentYearExtension" public="false">
<argument type="service" id="session" />
<argument>%current_year%</argument>
<tag name="twig.extension" />
</service>
And use it in your route generation:
{{ url('myapp.whatever', {year: current_year()}) }}
If you need the current year other places than twig, then pull a re-usable object out of the twig extension and use that both with the extension and elsewhere.
Thanks to the answer given by #chrisguitarguy : https://stackoverflow.com/a/13495302/1031898 I found a way to resolve my problem.
In fact, I could use my routeListener to do the job.
I just needed to implement the Router Interface.
LastRouteListener.php (updated)
class LastRouteListener
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event)
{
// Don't store subrequests
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$context = $this->router->getContext();
// If current_year exists in session, replace route parameter with it
if ($session->has('current_year')) {
$context->setParameter('year', $session->get('current_year'));
}
// Else, we set the current year by default
else {
$context->setParameter('year', date('Y'));
}
$routeName = $request->get('_route');
$routeParams = $request->get('_route_params');
if ($routeName[0] == "_") {
return;
}
$routeData = ['name' => $routeName, 'params' => $routeParams];
// On ne sauvegarde pas la même route plusieurs fois
$thisRoute = $session->get('this_route', []);
if ($thisRoute == $routeData) {
return;
}
$session->set('last_route', $thisRoute);
$session->set('this_route', $routeData);
}
}
Don't forget to inject the #router argument in services.yml
myapp.last_route_event_listener:
class: MyApp\EventListener\LastRouteListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }
arguments: ['#router']
And then, no need to use 'current_year' default parameter in route config anymore.
routes.yml
mes_routes:
resource: "mes_routes.yml"
prefix: /{year}
MyAppExtension.php
class MyAppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
I have used the GeniusesOfSymfony WebsocketBundle to integrate websockets into my system.
I am now trying to push a notification using the following code (this code is located in a symfony command)
protected function execute(InputInterface $input, OutputInterface $output)
{
$messageData = array(
'message' => $input->getArgument('message'),
'title' => $input->getOption('title') === null ? $this->title : $input->getOption('title'),
'timeout' => $input->getOption('timeout') === null ? $this->timeout : $input->getOption('timeout'),
);
$pusher = $this->getContainer()->get('gos_web_socket.zmq.pusher');
$pusher->push($messageData, 'broadcast');
$output->writeln('Message has been sent');
}
This works perfectly. However, how can I check if the push() function has actually pushed my message to the scoket server? I want to be able to output "Message has been sent" only if this is actually true.
Update
The GeniusesOfSymfony documentation learned me that there are two events to check if it has been a success or an error.
gos_web_socket.push_success
gos_web_socket.push_fail
But I think i can't just do:
if ($event('gos_web_socket.push_success')) {
$output->writeLn("Message has been sent");
} else {
$output->writeLn("Message has NOT been sent");
}
You should make yourself familiar with the event-dispatcher component of symfony. The underlying pattern is the Observer Pattern.
To put it short: Events are dispatched from a service (the subject) to enable other services (the observers) to react on them.
This is an example how you may implement this for gos_web_socket.push_success and gos_web_socket.push_fail
First, create your observers (EventListeners):
use Gos\Bundle\WebSocketBundle\Event\PushHandlerEvent;
class AcmeListener
{
// ...
public function onSuccess(PushHandlerEvent $event)
{
// ... do something
}
public function onFailure(PushHandlerEvent $event)
{
// ... do something
}
}
Register this listeners to the according events in your services.yml:
services:
acme.socket_listeners:
class: "AcmeListener"
tags:
- { name: kernel.event_listener, event: gos_web_socket.push_success, method: onSuccess }
- { name: kernel.event_listener, event: gos_web_socket.push_fail, method: onFailure }
This should give you a start.
Update:
As event-listeners are nothing more than callables (read: functions), you could implement them directly in your command as well to have access to your $output:
protected function execute(InputInterface $input, OutputInterface $output)
{
$messageData = array(
'message' => $input->getArgument('message'),
'title' => $input->getOption('title') === null ? $this->title : $input->getOption('title'),
'timeout' => $input->getOption('timeout') === null ? $this->timeout : $input->getOption('timeout'),
);
$pusher = $this->getContainer()->get('gos_web_socket.zmq.pusher');
$eventDispatcher = $this->getContainer()->get('event_dispatcher');
$eventDispatcher->addListener(
'gos_web_socket.push_success',
function (PushHandlerEvent $event) use ($output) {
$output->writeln('Message has been sent');
}
);
$pusher->push($messageData, 'broadcast');
}