I'm working with an external API webservice that returns a json output true or false. I visit a URL like
http://site.com/api/valid
and it gives me something like this, which looks like json
"true"
Right now I'm visiting the url manually, but I want to now do it programmatically from within my zend project. What should I use to get the result correctly
there are a lot of ways. The simplest is to use file_get_contents().
$result = file_get_contents("http://site.com/api/valid");
// if result is truly json
// data will be
// array( 0 => true)
$data = json_decode($result);
If it is a popular webservice . there might be a library written for it already. This is preferred since it will handle error conditions and corner cases. Google around for it first.
Sounds like you need a method that can grab the endpoint. Well, there's many ways but since you are already using Zend, you might as well read up on http://framework.zend.com/manual/1.11/en/zend.http.client.adapters.html
Here's a static method:
static function curl($url, $method, $params = array()){
$client = new Zend_Http_Client($url);
if($method == "POST"){
$client->setParameterPOST($params);
}else{
$client->setParameterGet($params);
}
$response = $client->request($method);
return $response->getBody();
}
Or use php's native method $response = file_get_contents($url);
Make sure to json_decode() your responses.
Related
I need to use an api in my website frontend, but I don't want to expose that api's key to my frontend users, so I've decided to make a proxy. However, I don't think I've necessarily done it in the most clean, straight-forward, Laravel-like or Guzzle-like way. I'll show my work:
In web.php I added a route that looks like this: Route::post('/address-api/{path?}', 'Controller#addressApi')->where('path', '.*'); That way, the entire path after /address-api is passed to my controller, so I can proxy hypothetically ANY post request to that api.
Then in Controller.php I've done this:
public function addressApi($path, Request $request)
{
if (!Str::startsWith($path, '/')) $path = '/' . $path; // make sure it starts with /
$url = 'https://api.craftyclicks.co.uk/address/1.1' . $path;
$postData = $request->all();
$postData['key'] = env('CRAFTYCLICKS_KEY');
$client = new Client();
$response = $client->request('POST', $url, [
'json' => $postData
]);
return response()->json(json_decode($response->getBody(), true));
}
So, whatever json they post to my api, I post to the CraftyClicks api, but I add our secret key to the json. The code above is working, it just doesn't seem like the right way to do it.
The thing I'm not sure about is json_decoding the body and returning it, return response()->json(json_decode($response->getBody(), true));. I feel like there's something... dirty about this. I feel like there must be a cleaner way to return the actual API response exactly as it came in.
At first I was doing return $response->getBody();, but I didn't like that because it didn't have the Content-type: application/json header in the response when I did it that way. Does Guzzle provide, out of the box, a way of just returning their response entirely as-is, headers and all?
Let Laravel have the output; this is cleaner.
return response($response->getBody())
->withHeaders($response->getHeaders());
Update
This seems to relate in some way to the reading of the stream when outputting. The function used by Slim to output the body looks like this, where $body implements StreamInterface and $this->responseChunkSize is 4096:
$amountToRead = $body->getSize();
while ($amountToRead > 0 && !$body->eof()) {
$length = min($this->responseChunkSize, $amountToRead);
$data = $body->read($length);
echo $data;
$amountToRead -= strlen($data);
if (connection_status() !== CONNECTION_NORMAL) {
break;
}
}
It appears the $body->eof() call (which is just a wrapper for PHP's feof() function) is returning true even though the full file has not been read. Not sure why that would be though. I also verified that this does not occur if I just do an fopen() on the file and create a Stream from it, then run the same code. It only happens when the stream is the product of the external REST API call via Guzzle.
Original Post
I have a service built using Slim (v4.4) that calls an external REST API using Guzzle (v6.5.3) that returns a file. This is running in Windows, web server is IIS/FastCGI (I know, unusual). PHP version is 7.3.10. The call from Slim to the external REST API retrieves the file just fine, but when my app calls the service, some files get corrupted, seems some data gets lost based on what I see in the file size. The call from the service to the external REST API is fairly simple:
$file_response = $guzzleClient->request('GET', "{$base_url}/docs/{$file_id}", [
'headers' => [
'Authorization' => "token {$token}"
]
]);
The above call works fine and returns the file correctly, I can either display it to screen or use the 'sink' option in Guzzle to save to a file, it works fine. But when I try to call the service that wraps that call, it fails. I tried a couple things. Firstly, I was just returning the response as is since it conforms to the interface required anyway. My Slim route looks like this:
$app->group('/files', function (Group $group) {
$group->get('/{file_id}', GetFileAction::class);
});
The GetFileAction class has a method like this:
public function __invoke(Request $request, Response $response, $args): Response {
...Guzzle request returning $file_response here...
return $file_response;
}
My app is also using Guzzle to call the service, the call looks like this:
$guzzleClient->request(
'GET',
"{$base_url}/files/{$file_id}",
[
'auth' => [$username, $password],
'sink' => $file_path
]
);
I wondered if returning the Guzzle response in Slim might be causing some unexpected result, so I tried returning this in the service instead:
return $response->withBody(new \Slim\Psr7\Stream($file_response->getBody()->detach()));
Same result. Obviously if somebody who has run into this exact same problem can help out it would be great, but if not some pointers on how I could try to debug the handling of the streams would likely be helpful.
I've confirmed this is linked to a weird issue with the feof() function returning true even though it hasn't read the full file. The solution I came up with involved creating a different Response Emitter than the default Slim 4 one (mostly the same) and overwrite the emitBody function so it does not rely on feof(). I did so like this:
$length = min($this->responseChunkSizeCopy, $amountToRead);
while ($amountToRead > 0 && ($data = $body->read($length)) !== false) {
echo $data;
$amountToRead -= $length;
$length = min($this->responseChunkSizeCopy, $amountToRead);
if (connection_status() !== CONNECTION_NORMAL) {
break;
}
}
So far this has worked well based on my testing. I have no idea why feof() is not working as expected and didn't really find anything that seemed to specifically address it. Maybe it's a Windows specific thing, and since PHP is less common on Windows it's not a common occurrence. But leaving this solution here in case it can help someone.
I'm trying to achieve a similar goal—using Slim to proxy and forward incoming requests to another service via a Guzzle client—and encountered a similar problem when returning the Guzzle response.
In my case the problem was that the other service was incorrectly returning a Transfer-Encoding: chunked header in the response.
Your mileage may vary, but the solution was to replace this with a correct Content-Length header in the returned response:
return $response
->withoutHeader('Transfer-Encoding')
->withHeader('Content-Length', $response->getBody()->getSize());
I played around with the PHP 7.2 runtime and HTTP trigger on Alibaba Cloud Function Compute. The basic example in the documentation is the following:
<? php
use RingCentral\Psr7\Response;
function handler($request, $context): Response{
/*
$body = $request->getBody()->getContents();
$queries = $request->getQueryParams();
$method = $request->getMethod();
$headers = $request->getHeaders();
$path = $request->getAttribute("path");
$requestURI = $request->getAttribute("requestURI");
$clientIP = $request->getAttribute("clientIP");
*/
return new Response(
200,
array(
"custom_header1" => "v1"
),
"hello world"
);
}
This works quite well. It's easy to get the query parameters from an URL. But the body content is only available in a whole string with
$request->getBody()->getContents();
Although the documentation says that the $request parameter follows the PSR-7 HTTP Message standard, it is not possible to use $request->getParsedBody() to deliver the values submitted by POST method. It didn't work as expected - the result remains empty.
The reason is the underlying technology. Alibaba Cloud Function Compute makes use of the event-driven React PHP library to handle the requests (you can check this by analyzing the $request object). So the $_POST array is empty and there is no "easy way to get POST data".
Luckily, Alibaba's Function Compute handler provides the body content by $request->getBody()->getContents(); as a string like
"bar=lala&foo=bar"
So a solution seems easiser than thought at the beginning, you can e.g. use PHP's own parse_str() function:
$data = [];
$body = $request->getBody()->getContents();
parse_str($body,$data);
If you place this snippet in the handler function, the POST variables are stored in the $data array and ready for further processing.
Hope that this helps somebody who asked the same questions than I. :-)
Kind regards,
Ralf
As you can see in the documentation you need to add a RequestBodyParserMiddleware as middleware to get a parsed PSR-7 request. It seems you didn't do that.
Also keep in mind that only the Content-Types: application/x-www-form-urlencoded and multipart/form-data are supported here. So make sure the client need to send these headers so the request can be parsed. If it's another Content-Type you need to use another middleware.
See: https://github.com/reactphp/http#requestbodyparsermiddleware for more information.
I hope this helps!
#legionth: I apologize that I didn't use the comment feature here, but my answer is too long. :-)
Thanks a lot for your comments - the usage of RequestBodyParserMiddleware is a great solution if you can control the server code. But in the context of Alibaba Cloud Function Compute service this seems not possible. I tried to find out more information about the invocation process - here are my results:
Function Compute makes use of the Docker image defined in https://github.com/aliyun/fc-docker/blob/master/php7.2/run/Dockerfile .
In the build process they download a PHP runtime environment from https://my-fc-testt.oss-cn-shanghai.aliyuncs.com/php7.2.tgz . (I didn't find this on GitHub, but the code is public downloadable.)
A shell script start_server.sh starts a PHP-CGI binary and runs a PHP script server.php.
In server.php a React\Http\Server is started by:
$server = new Server(function (ServerRequestInterface $request) {
[...]
});
[...]
$socket = new \React\Socket\Server(sprintf('0.0.0.0:%s', $port), $loop);
$server->listen($socket);
$loop->run();
As seen in the Function Compute documentation (& example of FC console), I can only use two functions:
/*
if you open the initializer feature, please implement the initializer function, as below:
*/
function initializer($context) {
}
and the handler function you can find in my first post.
Maybe Alibaba will extend the PHP runtime in future to make it possible to use a custom middleware, but currently I didn't find a way to do this.
Thanks again & kind regards,
Ralf
In my CakePHP app I return JSON and exit for certain requests. An example of this would be trying to access the API for a login as a GET request:
header('Content-Type: application/json');
echo json_encode(array('message'=>'GET request not allowed!'));
exit;
However I am having to prefix the echo with the content type in order for it to be sent as JSON. Otherwise my code at the other end interprets it different.
Any ideas on how to get around this? Or at least improve it.
Update: Cake version 2.3.0
You can leverage the new 2.x response object:
public function youraction() {
// no view to render
$this->autoRender = false;
$this->response->type('json');
$json = json_encode(array('message'=>'GET request not allowed!'));
$this->response->body($json);
}
See http://book.cakephp.org/2.0/en/controllers/request-response.html#cakeresponse
Also you could use the powerful rest features and RequestHandlerComponent to achieve this automatically as documented: http://book.cakephp.org/2.0/en/views/json-and-xml-views.html
You just need to allow the extension json and call your action as /controller/action.json.
Then cake will automatically use the JsonView and you can just pass your array in. It will be made to JSON and a valid response by the view class.
Both ways are cleaner than your "exit" solution - try to unit-test code that contains die()/exit(). This will end miserably. So better never use it in your code in the first place.
I am struggling to get PHP's inbuilt SoapClient to interpret the response coming back from the web service I'm trying to call.
SoapUI is able to interrogate this soap method and return good results.
I am also able to get nusoap_client to return correct results (but am not able to use nusoap for other reasons and think I'm stuck with SoapClient).
Using SoapClient, I can see that seemingly good data is being returned, but instead of the results being parsed and broken into easily consumed arrays of values, the XML response string is being stuffed into a single field in an object (labelled 'any').
My code and results are shown below :
$client = new SoapClient($url);
$results = $client->GetPropertiesByProjectAndContractStatus($params);
var_dump($results);
The output from the above code is below :
object(stdClass)[3]
public 'GetListingsByGUIDResult' =>
object(stdClass)[4]
public 'any' => string '<xs:schema xmlns="" ........ (long xml here) ....
Now, perhaps it's possible that the service I am using is returning some xml which has something wrong with it (although it seems fine to my eye). nusoap and SoapUI both have no problems using it either.
So I'm wondering what is it with SoapClient that is different.
I have a function that grabs that result and turns it into a dom object so you can use the dom functions to extract data.
protected function getElementsFromResult($elementName, $simpleresult) {
$dom = new DOMDocument ();
$dom->preserveWhiteSpace = FALSE;
if ($simpleresult == null) {
echo 'null';
return null;
} else {
$dom->loadXML ( $simpleresult->any );
return $dom->getElementsByTagName ( $elementName );
}
$elementName is the name of the elements you want from the result and $simpleresult is the object containing the 'any' string.
This happens when the data returned is not specified in the WSDL that you are using. Anything not in the WSDL will get lumped into this "any" element at the end of parsing the XML.
If this is happening then you should ensure that your script is using the correct WSDL for the SOAP service you're using.
For example, if you're using an old WSDL and new elements are now in use in the service, they will end up inside this "any" element!
Did you try using the SOAP_SINGLE_ELEMENT_ARRAYS feature?
<?php
$client = new SoapClient($url, array('features' => SOAP_SINGLE_ELEMENT_ARRAYS));