I'm playing around with the GuzzleHttp client, GuzzleCacheMiddleware and Memcached.
The setup is calling the same url with different parameters.
This results in one! memcached hit, so I think the memcached key is created from the url and only the url.
Can I somehow change this behaviour, so the key includes a md5 of the parameters?
You would have to create your own CacheStrategy class. For example you can extend PrivateCacheStrategy class and override getCacheKey method which is responsible for creating the cache key.
https://github.com/Kevinrob/guzzle-cache-middleware/blob/master/src/Strategy/PrivateCacheStrategy.php#L123
You are right that it creates storage key based on only the URL and request method.
Decided to look into it. You are right that it needs GreedyCacheStrategy because it literally caches everything regardless of any RFC standards.
Custom class for cache key creating.
class ParamsGreedyCacheStrategy extends GreedyCacheStrategy
{
/**
* Ignoring any headers, just straight up cache key based on method, URI, request body/params
*
* #param RequestInterface $request
* #param KeyValueHttpHeader|null $varyHeaders
* #return string
*/
protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
{
return hash(
'sha256',
'greedy' . $request->getMethod() . $request->getUri() . $request->getBody()
);
}
}
Creating requests. I used Laravel caching here, you can use memcached. I also allow POST HTTP method to be cached, because by default only GET is being cached!
$handlerStack = HandlerStack::create();
$cacheMiddleware = new CacheMiddleware(
new ParamsGreedyCacheStrategy(
new LaravelCacheStorage(
Cache::store('file')
),
10
)
);
// Not documented, but if you look at the source code they have methods for setting allowed HTTP methods. By default, only GET is allowed (per standards).
$cacheMiddleware->setHttpMethods(['GET' => true, 'POST' => true]);
$handlerStack->push(
$cacheMiddleware,
'cache'
);
$client = new Client([
'base_uri' => 'https://example.org',
'http_errors' => false,
'handler' => $handlerStack
]);
for($i = 0; $i < 4; $i++) {
$response = $client->post('/test', [
'form_params' => ['val' => $i]
]);
// Middleware attaches 'X-Kevinrob-Cache' header that let's us know if we hit the cache or not!
dump($response->getHeader('X-Kevinrob-Cache'));
}
Related
I've a problem using guzzleHttp with multiple requests. I want to get the path of an url after setup the request but before sending it. Here's my code:
include "../../vendor/autoload.php";
use GuzzleHttp\Client;
/* Initiate Guzzle Client */
$client = new Client([
"verify" => false, // disable ssl certificate verification
"timeout" => 30, // maximum timeout for requests
"http_errors" => false, // disable exceptions
]);
$requests = [];
$requests["a"] = $client->requestAsync('GET', "https://www.aaa.de/aaa.html");
$requests["b"] = $client->requestAsync('GET', "https://www.bbb.de/bbb.html");
$requests["c"] = $client->requestAsync('GET', "https://www.ccc.de/ccc.html");
$content = performMultiRequest($requests);
function performMultiRequest($requests)
{
foreach ($requests as $key => $object) {
print_r($object);
exit;
}
/**
* here comes more to send the requests, but that doesn't care for this problem
*/
}
In this case I get a GuzzleHttp\Promise\Promise Object. My goal is to get only the path /aaa.html from $object. It must be found inside the function performMultiRequest(). There's no chance to read and parse the url before, e.g. when the requestAsync() is used.
This is the relevant part of $object I need:
I tried SO, Guzzle documentation, google, trail & error... nothing found... any ideas?
I created a simple API in Lumen (application A) which:
receives PSR-7 request interface
replaces URI of the request to the application B
and sends the request through Guzzle.
public function apiMethod(ServerRequestInterface $psrRequest)
{
$url = $this->getUri();
$psrRequest = $psrRequest->withUri($url);
$response = $this->httpClient->send($psrRequest);
return response($response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
The above code passes data to the application B for the query params, x-www-form-urlencoded, or JSON content type. However, it fails to pass the multipart/form-data. (The file is available in the application A: $psrRequest->getUploadedFiles()).
Edit 1
I tried replacing the Guzzle invocation with the Buzz
$psr18Client = new Browser(new Curl(new Psr17Factory()), new Psr17Factory());
$response = $psr18Client->sendRequest($psrRequest);
but still, it does not make a difference.
Edit 2
Instances of ServerRequestInterface represent a request on the server-side. Guzzle and Buzz are using an instance of RequestInterface to send data. The RequestInterface is missing abstraction over uploaded files. So files should be added manually http://docs.guzzlephp.org/en/stable/request-options.html#multipart
$options = [];
/** #var UploadedFileInterface $uploadedFile */
foreach ($psrRequest->getUploadedFiles() as $uploadedFile) {
$options['multipart'][] = [
'name' => 'file',
'fileName' => $uploadedFile->getClientFilename(),
'contents' => $uploadedFile->getStream()->getContents()
];
}
$response = $this->httpClient->send($psrRequest, $options);
But still no luck with that.
What I am missing? How to change the request so files will be sent properly?
It seems that $options['multipart'] is taken into account when using post method from guzzle. So changing the code to the $response = $this->httpClient->post($psrRequest->getUri(), $options); solves the problem.
Also, it is important not to attach 'content-type header.
I'd like to authenticate our application user against Firebase/Firestore and then make a request to the storage as this user (i.e. not as the service account).
I know of two methods for the authentication:
Simple HTTP Request
$client = new GuzzleHttp\Client();
$responee = $client->request(
'POST',
'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=' . $key,
[
'headers' => [
'content-type' => 'application/json',
'Accept' => 'application/json'
],
'body' => json_encode([
'email' => $email,
'password' => $password,
'returnSecureToken' => true
]),
'exceptions' => false
]
);
Kreait SDK
$userRecord = $auth->verifyPassword($email, $password);
What I don't know is how to use this information to make a request to the storage.
Google Cloud Firestore SDK
StorageClient accepts a config key credentialsFetcher but I don't know how to use it. It accepts any object that implements FetchAuthTokenInterface. I've toyed with those that exist, even tried implementing my own that just passes on the idToken from the Simple HTTP Request method. No luck.
$credentialsFetcher = new myFetchAuthTokenImplementation($idToken);
$storage = new StorageClient([
'credentialsFetcher' => $credentialsFetcher,
]);
$bucket = $storage->bucket('my_bucket');
$object = $bucket->object('file_backup.txt');
print $object->downloadAsString();
use Google\Auth\FetchAuthTokenInterface;
class myFetchAuthTokenImplementation implements FetchAuthTokenInterface
{
private $token;
public function __construct(string $token)
{
$this->token = [
'access_token' => $token,
];
}
public function fetchAuthToken(callable $httpHandler = null)
{
return $this->token;
}
public function getCacheKey()
{
return null;
}
public function getLastReceivedToken()
{
return $this->token;
}
}
Kreait SDK
It seems it can fetch information from storage but only using the service account. Not my application user.
$firebaseFactory = (new Factory)->withServiceAccount(__DIR__.'/google-service-account.json');
$storage = $firebaseFactory->createStorage();
$imageUrl = $storage->getBucket()
->object('file_backup.txt')
I would need to re-initialize the $firebaseFactory with the application user record, something like this fictitious method $firebaseFactory = (new Factory)->withApplicationUser($userRecord);
Although I would like to use some SDK, any solution is fine, even with simple HTTP requests.
I would probably be able to implement this using the Google JavaScript SDK but I'd like to stick to PHP.
Your help is greatly appreciated.
As far as I know, the Kreait PHP SDK wraps the Google Cloud Storage REST API. If it does, it always accesses Storage with Administrative credentials, and there is no way to access it as a Firebase Authentication user account, nor to enforce the security rules for a specific user.
To access Cloud Storage as a Firebase Authentication user, you will have to authenticate client-side, and pass the resulting ID token to an SDK/API that enforces Firebase security rules for specific users. This means you'll have to use one of the client-side Firebase SDKs for accessing Cloud Storage, as there currently is no public REST API that exposes this functionality.
Currently, I am working on integrating a chat API (it is called pusher Chatkit) to an app I am working on. For the backend, PHP and laravel are being used, and postman is being used to test the server. When testing on Postman I am receiving a 500 internal server error with this exception:
{
"message": "You must provide an instance_locator",
"exception": "Chatkit\\Exceptions\\MissingArgumentException",
"file": "/Users/shaquilenoor/Desktop/chatapi/vendor/pusher/pusher-chatkit-server/src/Chatkit.php",
"line": 49,
"trace": [
in trace, many files/lines are referred to, so I left out as it is too much to fit in this post (you can ask if needed though and I can make a Gdrive link with it in)
The code leading up to the line + function being referred to is this:
<?php
namespace Chatkit;
use Chatkit\Exceptions\ChatkitException;
use Chatkit\Exceptions\ConfigurationException;
use Chatkit\Exceptions\ConnectionException;
use Chatkit\Exceptions\MissingArgumentException;
use Chatkit\Exceptions\TypeMismatchException;
use Firebase\JWT\JWT;
class Chatkit
{
protected $settings = array(
'scheme' => 'https',
'port' => 80,
'timeout' => 30,
'debug' => false,
'curl_options' => array(),
);
protected $logger = null;
protected $ch = null; // Curl handler
protected $api_settings = array();
protected $authorizer_settings = array();
protected $cursor_settings = array();
const GLOBAL_SCOPE = 'global';
const ROOM_SCOPE = 'room';
/**
*
* Initializes a new Chatkit instance.
*
*
* #param array $options Options to configure the Chatkit instance.
* instance_locator - your Chatkit instance locator
* key - your Chatkit instance's key
* scheme - e.g. http or https
* host - the host; no trailing forward slash.
* port - the http port
* timeout - the http timeout
*/
public function __construct($options)
{
$this->checkCompatibility();
if (!isset($options['instance_locator'])) {
throw new MissingArgumentException('You must provide an instance_locator');
}
if (!isset($options['key'])) {
throw new MissingArgumentException('You must provide a key');
}
$this->settings['instance_locator'] = $options['instance_locator'];
$this->settings['key'] = $options['key'];
$this->api_settings['service_name'] = 'chatkit';
$this->api_settings['service_version'] = 'v2';
$this->authorizer_settings['service_name'] = 'chatkit_authorizer';
$this->authorizer_settings['service_version'] = 'v2';
$this->cursor_settings['service_name'] = 'chatkit_cursors';
$this->cursor_settings['service_version'] = 'v2';
foreach ($options as $key => $value) {
// only set if valid setting/option
if (isset($this->settings[$key])) {
$this->settings[$key] = $value;
}
}
}
The instance_locator refers to a code generated on Pusher Chatkit, which I link in my files for integration of that API. This is my first time integrating a backend server using PHP so a little lost! Would appreciate some advice on where I should be looking to solve the issue and also more than happy to provide further info. Cheers
Depending on how you create API request you should do something like this:
$chatkit = new Chatkit(['instance_locator' => *locator*, 'key' => *actualKey*]);
The error you are getting means that you haven't passed variables in array.
I've tried to configure Pact for PHP using example configuration. My problem is I can run a mockServer, but every request I make returns 404 response. Of course I set everything up like in a GitHub readme. Still, I know server is visible (localhost config) but routes could not be registered.
Code example:
class PactTest extends \Tests\BaseTestCases\V2TestCase
{
/** #var MockServerConfig */
private $config;
public function setUp()
{
// Create your basic configuration. The host and port will need to match
// whatever your Http Service will be using to access the providers data.
$this->config = new MockServerConfig();
$this->config->setHost('localhost');
$this->config->setPort(7200);
$this->config->setConsumer('someConsumer');
$this->config->setProvider('someProvider');
$this->config->setHealthCheckTimeout(60);
$this->config->setCors(true);
// Instantiate the mock server object with the config. This can be any
// instance of MockServerConfigInterface.
$server = new MockServer($this->config);
// Create the process.
$server->start();
// Stop the process.
$server->stop();
}
public function testSimple()
{
$matcher = new Matcher();
// Create your expected request from the consumer.
$request = new ConsumerRequest();
$request
->setMethod('GET')
->setPath('/test/abc')
->addHeader('Content-Type', 'application/json');
// Create your expected response from the provider.
$response = new ProviderResponse();
$response
->setStatus(200)
->addHeader('Content-Type', 'application/json;charset=utf-8')
->setBody([
'message' => $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]')
]);
// Create a configuration that reflects the server that was started. You can
// create a custom MockServerConfigInterface if needed. This configuration
// is the same that is used via the PactTestListener and uses environment variables.
$builder = new InteractionBuilder($this->config);
$builder
->given('a thing exists')
->uponReceiving('a get request to /test/abc')
->with($request)
->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction.
$service = new HttpClientService($this->config->getBaseUri()); // Pass in the URL to the Mock Server.
$result = $service->getTestAbc(); // Make the real API request against the Mock Server.
$builder->verify();
self::assertEquals('Hello, Bob', $result); // Make your assertions.
}
Where getTestAbc() is:
public function getTestAbc(): string
{
$uri = $this->baseUri;
$response = $this->httpClient->get("{$uri->getHost()}/test/abc", [
'headers' => ['Content-Type' => 'application/json']
]);
$body = $response->getBody();
$object = \json_decode($body);
return $object->message;
}
What do I do wrong?
You're stopping the mock server in setUp. You should stop the server after the test in tearDown. I've noticed that's the code from the manual and it may be quite misleading, but I think it was intended as an example how to start/stop mock server by hand.