I have the following route:
Route::get('echo',function (Request $req) {
return response()->stream(function () use ($req) {
echo json_encode($req->all());
}, 200, [
'Content-Type' => 'application/json'
]);
})->name('echo');
For the sake of simplicity lets assume it's a simple echo response. In reality it's a very large file. The outcome in either case is the same.
Now I want to test this route to see whether I indeed can see that json content so I've tried this:
public function testBasicTest()
{
$response = $this->get(route('echo', [
"content"=>"some content"
]));
$response->assertSeeText("some content"); //Doesn't work
$response->assertJson( [
"content"=>"some content"
]); //Neither does this
}
I've inspected it a bit further and it appears to be because (a) the response is wrapped around a TestResponse (b) the response content is never streamed and (c) even if the response content were to be forcibly streamed via $response->baseResponse->sendContent() the actual content is echoed and not actually captured by the TestResponse
In addition calling $response->getContent() does not work because it seems to directly call the StreamedResponse::getContent() which is hard-coded to return false.
I have managed to have some limited success using:
ob_start();
$response->sendContent();
$result = ob_get_clean();
however this looks like a very sloppy thing to do in a unit test.
Has anyone else encountered this before? Is it possible to test the contents of a streamed response?
This is not a good solution, more of a hack, but if anyone else encounters this issue here's what you can do:
public function testBasicTest()
{
$response = $this->get(route('echo', [
"content"=>"some content"
]));
if ($response->baseResponse instanceof StreamedResponse) {
ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response($content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);
}
$response->assertSee("some content"); //Works
}
A bit late to the party but it may help someone else.
In Laravel you can do $response->streamedContent() when handling a StreamedResponse (since 5.8 I believe).
Even tho my debugger told me the streamed content of my response was null I still got the data out of it.
In my case it was CSV so in my functional tests I've done :
$res = $this->post('api/v1/entity/export', $payload, $header);
$res->assertStatus(200);
$res->assertHeader('Content-Disposition', 'attachment; filename=entity.csv');
$reader = Reader::createFromString($res->streamedContent());
// tests...
I used LeagueCSV (bc it was CSV, obviously) but I'm sure you can do the same with Json or other.
Laravel doc for TestResponse
Related
I'm new to PHPUnit and wondering is it possible to write a test for which ignore specific method.
The code is like examine whether $data is Valid or not, and if it find irregular data, send message to slack with it.
My question is, is it possible to run a test without sending alert message, like ignore sendAlert function?
If possible, I want to know how to write it, If not, I want know why and how to make this code testable.
Thanks!!
example code )
public static function isValid($data) {
// some code here
if (Valid) {
return true;
} else {
// some code here to find irregular
if (irregular) {
self::sendAlert($data);
}
return false;
}
}
private static function sendAlert($data) {
// send alert to slack
Example_Model_Slack::post($slackMsg, $channel);
}
<?
class Example_Model_Slack
{
public static function post($text, $channel = '') {
// make $params from $text and $channel
// POST
$stream = [
'http' => [
'method' => 'POST',
'protocol_version' => 1.1,
'content' => http_build_query($params),
],
];
return file_get_contents(self::POST_URL, false, stream_context_create($stream));
}
}
Edit after the question edit
If your code is in a namespace (which should be, it's good practice), it's extremely easy:
Create a new function in a separate file that is only included by your UnitTest file. This file should have the same namespace as your code. In this example, Example_Model_Slack is in the namespace Foobar\Models.
<?php
namespace Foobar\Models;
function file_get_contents(string $filename, bool $use_include_path = false, resource $context = ?)
{
return 'Whatever you want';
}
When you call a function, the code looks for it:
In the specifically used functions.
In the same namespace.
In the built-in functions.
Therefore, your code will use the built-in file_get_contents (namely \file_get_contents), but your test will use the one in the same namespace (namely \Foobar\Models\file_get_contents).
Original answer
The easiest would be to actually call sendAlert, but to mock the call to its content. As you didn't provide the code of that method, I can't be more precise, juste browse through the doc and figure it out by yourself or, alternatively, show us the code.
For a theorectical and general answer: your sendAlert method probably uses one that is provided by an external vendor, let's say \SlackApi\Slack::send($message). In that case, you could mock the provided \SlackApi\Slack class to replace the send method with one that doesn't actually send anything but still returns the expected data.
I am using the below sample code to return data to the browser
$response->write($image);
return $response->withHeader('Content-Type', 'image/jpeg');
which works fine but how would i go about it if i wanted to also return Content-Length ?
the only option i found so far is to copy the response into a new object like below but i don't think that's efficient if i have a $image in the response.
$response = $response->withAddedHeader('Content-Length', strlen($image));
I tried it as array but that doesn't work..
Quoting from Slim 3 docs
Reminder
Unlike the withHeader() method, this method appends the new value to the set of values that already exist for the same header name. The Response object is immutable. This method returns a copy of the Response object that has the appended header value.
Both withHeader() and with appendedHeader() methods return a copy of the response object. So even if your do not assign the return value of $response->withHeader() to a variable and return the result directly, you are still working with a copy of the response object.
Regarding your concern about efficiency, you should use streams instead of string as response body. The following is an example of how to use streams for returning an image as the response:
<?php
use Slim\App;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Http\Body;
return function (App $app) {
$container = $app->getContainer();
$app->get('/', function(Request $request, Response $response) {
$image = fopen('sample.jpg', 'r');
return $response->withBody(new Body($image))
->withHeader('Content-Type', 'image/jpeg')
->withAddedHeader('Content-Length', fstat($image)['size']);
});
};
Disclaimer: This is my first time working with ReactPHP and "php promises", so the solution might just be staring me in the face 🙄
I'm currently working on a little project where I need to create a Slack bot. I decided to use the Botman package and utilize it's Slack RTM driver. This driver utilizes ReactPHP's promises to communicate with the RTM API.
My problem:
When I make the bot reply on a command, I want to get the get retrieve the response from RTM API, so I can cache the ID of the posted message.
Problem is that, the response is being returned inside one of these ReactPHP\Promise\Promise but I simply can't figure out how to retrieve the data.
What I'm doing:
So when a command is triggered, the bot sends a reply Slack:
$response = $bot->reply($response)->then(function (Payload $item) {
return $this->reply = $item;
});
But then $response consists of an (empty?) ReactPHP\Promise\Promise:
React\Promise\Promise^ {#887
-canceller: null
-result: null
-handlers: []
-progressHandlers: & []
-requiredCancelRequests: 0
-cancelRequests: 0
}
I've also tried using done() instead of then(), which is what (as far as I can understand) the official ReactPHP docs suggest you should use to retrieve data from a promise:
$response = $bot->reply($response)->done(function (Payload $item) {
return $this->reply = $item;
});
But then $response returns as null.
The funny thing is, during debugging, I tried to do a var_dump($item) inside the then() but had forgot to remove a non-existing method on the promise. But then the var_dump actually returned the data 🤯
$response = $bot->reply($response)->then(function (Payload $item) {
var_dump($item);
return $this->reply = $item;
})->resolve();
So from what I can fathom, it's like I somehow need to "execute" the promise again, even though it has been resolved before being returned.
Inside the Bot's reply method, this is what's going on and how the ReactPHP promise is being generated:
public function apiCall($method, array $args = [], $multipart = false, $callDeferred = true)
{
// create the request url
$requestUrl = self::BASE_URL . $method;
// set the api token
$args['token'] = $this->token;
// send a post request with all arguments
$requestType = $multipart ? 'multipart' : 'form_params';
$requestData = $multipart ? $this->convertToMultipartArray($args) : $args;
$promise = $this->httpClient->postAsync($requestUrl, [
$requestType => $requestData,
]);
// Add requests to the event loop to be handled at a later date.
if ($callDeferred) {
$this->loop->futureTick(function () use ($promise) {
$promise->wait();
});
} else {
$promise->wait();
}
// When the response has arrived, parse it and resolve. Note that our
// promises aren't pretty; Guzzle promises are not compatible with React
// promises, so the only Guzzle promises ever used die in here and it is
// React from here on out.
$deferred = new Deferred();
$promise->then(function (ResponseInterface $response) use ($deferred) {
// get the response as a json object
$payload = Payload::fromJson((string) $response->getBody());
// check if there was an error
if (isset($payload['ok']) && $payload['ok'] === true) {
$deferred->resolve($payload);
} else {
// make a nice-looking error message and throw an exception
$niceMessage = ucfirst(str_replace('_', ' ', $payload['error']));
$deferred->reject(new ApiException($niceMessage));
}
});
return $deferred->promise();
}
You can see the full source of it here.
Please just point me in some kind of direction. I feel like I tried everything, but obviously I'm missing something or doing something wrong.
ReactPHP core team member here. There are a few options and things going on here.
First off then will never return the value from a promise, it will return a new promise so you can create a promise chain. As a result of that you do a new async operation in each then that takes in the result from the previous one.
Secondly done never returns result value and works pretty much like then but will throw any uncaught exceptions from the previous promise in the chain.
The thing with both then and done is that they are your resolution methods. A promise a merely represents the result of an operation that isn't done yet. It will call the callable you hand to then/done once the operation is ready and resolves the promise. So ultimately all your operations happen inside a callable one way or the other and in the broadest sense. (Which can also be a __invoke method on a class depending on how you set it up. And also why I'm so excited about short closures coming in PHP 7.4.)
You have two options here:
Run all your operations inside callable's
Use RecoilPHP
The former means a lot more mind mapping and learning how async works and how to wrap your mind around that. The latter makes it easier but requires you to run each path in a coroutine (callable with some cool magic).
Hello fellow coders !
I am looking for a solution to show a html page while my php code prepares a .zip which is then downloaded. The reason is because sometimes the zips are quite bigger and take time to make.
The HTML page would be a basic "Please wait while your .zip files is being prepared".
The PHP side used is Symfony. So I come into my getInboxExportAction function by calling https://myapi.com/orders/orderid/inbox/export.
The download function (makeExportDownloadRequestResponse) works fine. But if I add a flush after making my $response, the .zip is printed to the html, instead of being downloaded...
public function getInboxExportAction(Request $request, $orderId)
{
$response = new Response($this->twig->render('base.html.twig',
['content' => '
<h1>Download</h1>
<p>Your zip is being prepared for download, please wait...</p>
']));
$response->headers->set('Content-Type', 'text/html');
//Here I would like to echo + flush my html.
//Then stop the flush and continue my code
$receivedOrder = $this->fetchRequestedReceivedOrder($orderId);
if (!$receivedOrder instanceof ReceivedOrder) {
return $receivedOrder;
}
if (!$receivedOrder->getSentorder()) {
$this->makeErrorResponse('Sent order was not found', Response::HTTP_BAD_REQUEST);
}
return $this->makeExportDownloadRequestResponse($receivedOrder->getSentorder(), $request->get('format'));
}
I am also very open to any other ideas anyone would have to fix this issue :)
Thanks,
Edit : My $this->makeExportDownloadRequestResponse() function returns a response, not a path. We unlink the file on the server for storage reasons.
$content = file_get_contents($zipTmpFile);
unlink($zipTmpFile);
$response = new Response($content);
$dispositionHeader = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT,
'myFile_' . date('Y_m_d_h_i_s') . '.zip');
$response->headers->set('Content-Disposition', $dispositionHeader);
return $response;
Second edit : I understand that what I'm trying to do (change the content-type within one call) I generally frowned upon. I'm currently trying to think of a more elegant solution.
In your getInboxExportAction, just return a response with your text.
Then, in this response, add a <meta http-equiv="refresh" content="1;url=xxx"> tag to redirect the user to another action that will generate the zip file.
You can also handle this redirection with javascript.
Note that you can use a StreamedResponse to handle the zip download: https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response
The most important thing is you set $response->headers->set('Content-Type', 'text/html');, the browser will not download the .zip file.
If your makeExportDownloadRequestResponse method can return the absolute .zip file path (or you can calculate it), you can try:
public function getInboxExportAction(Request $request, $orderId)
{
$response = new Response($this->twig->render('base.html.twig',
['content' => '
<h1>Download</h1>
<p>Your zip is being prepared for download, please wait...</p>
']));
$response->headers->set('Content-Type', 'text/html');
//Here I would like to echo + flush my html.
//Then stop the flush and continue my code
$receivedOrder = $this->fetchRequestedReceivedOrder($orderId);
if (!$receivedOrder instanceof ReceivedOrder) {
return $receivedOrder;
}
if (!$receivedOrder->getSentorder()) {
$this->makeErrorResponse('Sent order was not found', Response::HTTP_BAD_REQUEST);
}
// prepare the to be downloaded zip file
// then, do sth to get the .zip file path
$zipFilePath = $this->makeExportDownloadRequestResponse($receivedOrder->getSentorder(), $request->get('format'));
// re-set header
header('Content-Type: application/zip');
header('Content-disposition: attachment; filename=whateverName.zip');
header('Content-Length: ' . filesize($zipFilePath));
readfile($zipFilePath);
}
It will let browser downloads the zip file.
Finally, If your function is work as an API, and you need a frontend JS to execute it, try with Blob class on JS.
You can indeed use a StreamedResponse to get it rid of this issue. About the fact that the whole .wip file content being displayed because of the flush, try to call ob_flush before
I need to return this:
return header('Content-Type: image/png');
echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=');
by using FOSRestBundle but I don't know how to handle the echo in there. I have this code:
public function getTrackingEmailAction(ParamFetcher $paramFetcher, Request $request)
{
$view = View::create();
$response = [];
$view->setData($response)->setHeader('Content-Type', 'image/png')->setStatusCode(200);
return $view;
}
How do I send the base64 for display the 1x1 pixel? Do I need a view? (twig view)
According to the upper code snippet, you don’t have to return Base64. Instead, the opposite is true: you have to return the raw image data, which is merely Base64-encoded as having binary stuff inside PHP code is not so nice ;-)
In your controller action, you therefore only have to return a proper Response with the image data as content
return new Response(base64_decode('...'), 200, ['Content-Type' => 'image/png']);