Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DrupalCon Austin - A Tour of Guzzle

DrupalCon Austin - A Tour of Guzzle

Michael Dowling

June 04, 2014
Tweet

More Decks by Michael Dowling

Other Decks in Programming

Transcript

  1. I’m a SDE at AWS SDK for PHP I work

    on the Guzzle I created and many other projects...
  2. What we’ll cover • Drupal and Guzzle? • Guzzle 101

    • Guzzle 4.x • Event system • Testing
  3. + Guzzle? • HTTP client of Drupal 8 • Aggregator,

    Update, XML-RPC • Replaced drupal_http_request()
  4. drupal_http_request() Drupal's current outgoing-HTTP capability is, to be polite, minimal.

    We have one small function with a lousy API that can do basic requests, but that's it. If we want to be serious about web services we need strong bidirectional HTTP support. - boombatower https://drupal.org/node/1447736 “
  5. Why not X? • file_get_contents • raw cURL • PECL

    HTTP • Duplication Or, why was Guzzle created?
  6. What about a different HTTP client? • @mcarper did a

    comparison • persistent connections • parallel requests • cookies • streaming...
  7. Who else uses Guzzle? • AWS SDK for PHP, Goutte,

    Tumblr, ... • ~1,000 Packagist packages deps • ~1.7 million Packagist downloads
  8. Guzzle sends requests require 'vendor/autoload.php'; ! $client = \Drupal::httpClient(); !

    $response = $client->post('http://httpbin.org/post', [ 'body' => ['foo' => 'bar'], 'headers' => ['X-Foo' => 'Bar'] ]); This sends a POST request
  9. HTTP request from the client POST /post HTTP/1.1 Host: httpbin.org

    User-Agent: Guzzle/4.0.2 curl/7.21.4 PHP/5.5.8 X-Foo: Bar Content-Type: application/x-www-form-urlencoded Content-Length: 7 ! foo=bar
  10. HTTP response from the server HTTP/1.1 200 OK Access-Control-Allow-Origin: *

    Content-Type: application/json Date: Thu, 15 May 2014 00:43:16 GMT Server: gunicorn/0.17.4 Content-Length: 478 Via: 1.1 sea3-proxy-5.amazon.com:80 (Cisco- IronPort-WSA/7.5.2-351) Connection: keep-alive ! {"form": {"foo": "bar"}, [...snipped...]}
  11. Working with responses echo $response->getStatusCode(); // 200 echo $response->getReasonPhrase(); //

    OK echo $response->getHeader('Content-Type'); // application/json echo $response->getHeader('Content-Length'); // 478 echo $response->getBody(); // {"form":{"foo":"bar"},"origin":"72.21....
  12. Response bodies are Guzzle streams $body = $response->getBody(); ! echo

    $body->getSize(); // 478 var_export($body->tell()); // 0 var_export($body->isReadable()); // true ! while (!$body->eof()) { echo $body->read(1024); }
  13. HTTP methods $res = $client->delete('http://httpbin.org/delete'); $res = $client->head('http://httpbin.org/get'); $res =

    $client->options('http://httpbin.org/get'); $res = $client->patch('http://httpbin.org/patch'); $res = $client->post('http://httpbin.org/post'); $res = $client->put('http://httpbin.org/put'); #MethodMan
  14. Creating request objects $url = 'http://httpbin.org/get'; $request = $client->createRequest('GET', $url);

    ! $request->setHeader('User-Agent', 'Testing!'); $request->setHeader('X-Foo', ['Baz', 'Bar']); $request->addHeader('X-Foo', 'Bam'); echo $request->getHeader('X-Foo'); // Baz, Bar, Bam $request->removeHeader('x-foo'); echo $request->getHeader('X-Foo'); // '' ! $res = $client->send($request);
  15. Swappable Adapters • Uses cURL by default • Not always

    available • Sometimes has version specific issues • Stream wrapper as fallback & for streaming
  16. 3.x vs 4.x 3.x 4.x Internal methods protected private Parallel

    requests Batching Async rolling queues Exceptions Lots of markers, inconsistent Minimal and consistent
  17. headers $response = $client->get('http://httpbin.org/get', [ 'headers' => [ 'X-Foo' =>

    'bar', 'X-Baz' => ['a', 'b', 'c'] ] ]); • Can be strings • Can be arrays or associative arrays
  18. timeout $response = $client->get('http://httpbin.org/get', [ 'timeout' => 10, 'connect_timeout' =>

    2, ]); • Total time in seconds • connect_timeout: Connection time
  19. Adding request options to every request $client = new Client([

    'defaults' => [ 'headers' => ['Foo' => 'Bar'], 'query' => ['testing' => '123'], 'auth' => ['username', 'password'], 'proxy' => 'tcp://localhost:80' ] ]); • Use defaults in a client constructor
  20. body // Send from a string $response = $client->put('http://httpbin.org/put', [

    'body' => 'hi!' ]); ! // Send from a PHP stream $response = $client->put('http://httpbin.org/put', [ 'body' => fopen('/path/to/file.json', 'r') ]); ! // Send from a Guzzle Stream $response = $client->put('http://httpbin.org/put', [ 'body' => \GuzzleHttp\Stream\create() ]);
  21. save_to // Save to a file $response = $client->get('http://httpbin.org/get', [

    'save_to' => '/path/to/file.json' ]); • Just like “body” • Can save to Stream objects or files
  22. query $request = $client->get('http://httpbin.org/get', [ 'query' => [ 'foo' =>

    'bar', 'baz' => ['bam' => 'boo'] ] ]); • Associative array or Query object
  23. debug $request = $client->get('http://httpbin.org/get', [ 'debug' => true ]); •

    Outputs debug information emitted from the utilized HTTP adapter.
  24. * About to connect() to httpbin.org port 80 (#0) *

    Trying 184.72.242.196... * connected * Connected to httpbin.org (184.72.242.196) port 80 (#0) > GET /get HTTP/1.1 Host: httpbin.org User-Agent: Guzzle/4.0.2 curl/7.21.4 PHP/5.5.8 ! < HTTP/1.1 200 OK < Access-Control-Allow-Origin: * < Content-Type: application/json < Date: Thu, 15 May 2014 02:10:03 GMT < Server: gunicorn/0.17.4 < Content-Length: 294 < Via: 1.1 sea3-proxy-3.amazon.com:80 (Cisco-IronPort-WSA/7.5.2-351) < Connection: keep-alive < * Connection #0 to host httpbin.org left intact <http://httpbin.org/get> [CONNECT] <http://httpbin.org/get> [MIME_TYPE_IS] message: "application/json" <http://httpbin.org/get> [FILE_SIZE_IS] message: "Content-Length: 294" bytes_max: "294" <http://httpbin.org/get> [PROGRESS] bytes_max: "294" cURL adapter PHP stream wrapper adapter
  25. Other request options • allow_redirects • auth • events •

    subscribers • exceptions • verify • cert • ssl_key • expect • version • config Note: implemented in MessageFactory
  26. Event System • Request lifecycle events • Intercept requests and

    errors • “fork” of Symfony EventDispatcher
  27. Getting Emitters $client = new GuzzleHttp\Client(); $emitter = $client->getEmitter(); !

    $url = 'http://httpbin.org'; $request = $client->createRequest('GET', $url); $emitter = $request->getEmitter(); • Clients and requests have them • Listeners and subscribers added to a client are added to every request
  28. Adding Listeners use GuzzleHttp\Event\BeforeEvent; ! $emitter->on('before', function (BeforeEvent $event) {

    echo $event->getRequest(); }); $emitter->on($name, $fn, [$priority]);
  29. Listener Priorities all events • Be deliberate • Use “landmark”

    priorities • GuzzleHttp\Event\RequestEvents::* EARLY LATE
  30. Listener Priorities before • Be deliberate • Use “landmark” priorities

    • GuzzleHttp\Event\RequestEvents::* PREPARE_REQUEST SIGN_REQUEST
  31. Listener Priorities complete error / • Be deliberate • Use

    “landmark” priorities • GuzzleHttp\Event\RequestEvents::* VERIFY_RESPONSE REDIRECT_RESPONSE
  32. Event Subscribers • Collection of listeners • Easy to distribute

    • getEvents() • Adds client behavior at runtime
  33. Intercepting Events • before, complete, and error • Caching •

    Signing requests • Error handling • Retries • Stops event propagation
  34. Intercepting Events use GuzzleHttp\Event\BeforeEvent; use GuzzleHttp\Message\Response; ! $emitter->on('before', function (BeforeEvent

    $e) { if ($e->getRequest()->getPath() == '/foo') { $response = new Response(200); $e->intercept($response); } }, GuzzleHttp\Event\RequestEvents::EARLY);
  35. Completed Requests use GuzzleHttp\Event\CompleteEvent; ! $emitter->on('complete', function (CompleteEvent $e) {

    $total = $e->getTransferInfo('total_time'); ! if ($total > 5) { error_log('Request to ' . $e->getRequest()->getUrl() . ' was slow'); } }, GuzzleHttp\Event\RequestEvents::EARLY); Example: Log slow requests
  36. Error Events use GuzzleHttp\Event\ErrorEvent; ! $emitter->on('error', function (ErrorEvent $e) {

    if ($e->getResponse()) { return; } ! $tries = $request->getConfig()->get('retries'); $request->getConfig()->set('retries', ++$tries); ! if ($tries <= 3) { $response = $e->getClient()->send($request); $e->intercept($response); } }); Example: Retry connection errors 3 times
  37. Event “Loop” • before -> headers -> complete • before

    -> error • before -> headers -> complete -> error • before -> headers-> error
 -> (before -> h -> c): intercept()
 -> complete
  38. Event Subscribers • Mock • History • Redirect • Cookie

    • OAuth • Log • Retry • Transfer Progress • HTTP Cache • Message Integrity Built-in External
  39. $client = new \GuzzleHttp\Client([ 'base_url' => 'https://api.twitter.com/1.1/', 'auth' => 'oauth'

    ]); ! $oauth = new \GuzzleHttp\Subscriber\Oauth\Oauth1([ 'consumer_key' => 'my_key', 'consumer_secret' => 'my_secret', 'token' => 'my_token', 'token_secret' => 'my_token_secret' ]); ! $client->getEmitter()->attach($oauth); $res = $client->get('statuses/home_timeline.json'); OAuth and Twitter
  40. Good tests... • Are fast • Are predictable • No

    external dependencies • Work on a plane { No network access
  41. Good tests... • Use mocks • Use the Mock subscriber

    • Use the MockAdapter • Use the History subscriber
  42. Using MockAdapter use GuzzleHttp\Client; use GuzzleHttp\Adapter\MockAdapter; use GuzzleHttp\Adapter\TransactionInterface; use GuzzleHttp\Message\Response;

    ! $mockAdapter = new MockAdapter( function (TransactionInterface $trans) { // You have access to the request $request = $trans->getRequest(); // Return a response return new Response(200); }
 ); ! $client = new Client(['adapter' => $mockAdapter]);
  43. Use the Mock Subscriber • Built-in to core • Queue

    of responses • Queue of exceptions
  44. Mock Subscriber use GuzzleHttp\Subscriber\Mock; ! $mock = new Mock(); $mock->addResponse("HTTP/1.1

    200\r\nOK\r\n\r\n"); $mock->addException($myException); ! $client->getEmitter()->attach($mock); ! echo $client->get('/foo')->getStatusCode(); // 200 $client->get('/foo'); // Exception!
  45. History Subscriber use GuzzleHttp\Subscriber\History; ! $history = new History(); $client->getEmitter()->attach($mock);

    ! $client->get('/foo'); $client->get('/bar'); ! echo count($history); // 2 echo $history->getLastRequest(); // GET /bar HTTP/1.1 [...] echo $history->getLastResponse()->getStatusCode); // HTTP/1.1 200 OK [...]
  46. Summary • Why Guzzle • Basics of Guzzle • Why

    4.0 is better than 3.0 • Event system • Testing
  47. A Tour of Guzzle Michael Dowling DrupalCon Austin, 2014 Questions?

    https://austin2014.drupal.org/session/tour-guzzle