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

Guzzle - SymfonyLive Portland

Guzzle - SymfonyLive Portland

Michael Dowling

June 13, 2013
Tweet

More Decks by Michael Dowling

Other Decks in Programming

Transcript

  1. IUUQTUSFBN XSBQQFS • Need to work with stream contexts •

    No support for persistent connections • No support for parallel requests • Data must be loaded into memory • Easy to send simple GET requests • Built-in to PHP
  2. <?php // Send a GET request $url = 'http://www.amazon.com/'; $responseBody

    = file_get_contents($url); // Get headers from a magical variable $wtfHeaders = $http_response_header; IUUQTUSFBN XSBQQFS
  3. // Send a POST request $context = stream_context_create([ 'http' =>

    [ 'method' => 'POST', 'content' => http_build_query([ 'foo' => 'bar' ]) ) ]); $body = file_get_contents($url, false, $context); IUUQTUSFBN XSBQQFS
  4. D63- • Fast • Supports HTTP/1.1 • Supports streaming requests

    & responses • Can do pretty much anything • Almost everyone uses it • Very cumbersome API
  5. D63- $ch = curl_init('http://example.com'); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_UPLOAD, true);

    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Transfer-Encoding: chunked']); // Open a stream to download the contents of Amazon.com $stream = fopen('http://www.amazon.com', 'r'); // Use a callback to read the body curl_setopt( $ch, CURLOPT_READFUNCTION, function ($ch, $fd, $length) use ($stream) { return fread($stream, $length) ? ''; } ); curl_exec($ch);
  6. 1&$-)551 • Supports HTTP/1.1 • Supports streaming requests • Does

    not stream responses • Requires uncommon extension
  7. (V[[MF • Supports HTTP/1.1 • Parallel & persistent requests •

    Streaming requests and responses • Extensible with plugins • Web service framework
  8. D63-BTB EFQFOEFODZ • Popular libs require cURL • AWS, Magento,

    Facebook, Google, ... • Most shared hosts include it • GoDaddy, DreamHost, ServerGrove...
  9. D63-BTB EFQFOEFODZ • Popular libs require cURL • AWS, Magento,

    Facebook, Google, ... • Most shared hosts include it • GoDaddy, DreamHost, ServerGrove... d.JOTUBMMT http://daniel.haxx.se/blog/2012/05/16/300m-users/
  10. (V[[MF$PNQPOFOUT guzzle/common guzzle/http guzzle/parser guzzle/batch guzzle/cache guzzle/inflection guzzle/iterator guzzle/log guzzle/plugin

    guzzle/plugin-­‐async guzzle/plugin-­‐backoff guzzle/plugin-­‐cache guzzle/plugin-­‐cookie guzzle/plugin-­‐curlauth guzzle/plugin-­‐error-­‐response guzzle/plugin-­‐history guzzle/plugin-­‐log guzzle/plugin-­‐md5 guzzle/plugin-­‐mock guzzle/plugin-­‐oauth guzzle/service guzzle/stream { "require": { "guzzle/http": "3.*" } } Minimal installation Piece by piece { "require": { "guzzle/http": "3.*", "guzzle/plugin-mock": "3.*" } }
  11. • AWS SDK for PHP • Drupal 8 • Goutte

    • Tumblr (V[[MFJOUIF8JME
  12. • Guzzle is now part of Drupal 8 core •

    Replaced drupal_http_request() [2] • Evaluated HTTP libs & chose Guzzle [1] • Helped make Guzzle better [3] (V[[MF%SVQBM [1] http://groups.drupal.org/node/233173 [2] http://drupal.org/node/1862398 [3] https://github.com/guzzle/guzzle/contributors
  13. • Clients are the glue • Clients create and send

    Requests • Base URLs (V[[MFa)UUQa$MJFOU
  14. use Guzzle\Http\Client; // Create a client that uses a URI

    template $client = new Client( 'https://api.twitter.com/{version}', ['version' => '1.1'] ); 63*UFNQMBUFT RFC  6570
  15. $client = new Guzzle\Http\Client(); $url = 'http://www.amazon.com?foo=bar'; $response = $client->get($url)->send();

    4JNQMF(&5 SFRVFTU GET  /?foo=bar  HTTP/1.1 Host:  www.amazon.com User-­‐Agent:  Guzzle/3.5.0  curl/7.21.4  PHP/5.3.15
  16. DEFINT A-Z OPTION BASE 1 ' Include the Guzzle TYPE

    definitions '$INCLUDE: 'GUZZLE.BI' DIM CLIENT AS GuzzleClient DIM REQUEST AS GuzzleRequest DIM RESPONSE AS GuzzleResponse ' Initialize the client and request CLIENT.BASE_URL = "http://www.amazon.com" ' Create a request with a relative base URL REQUEST.URL = "?foo=bar" ' Response is passed by reference GuzzleSend (CLIENT, REQUEST, RESPONSE) PRINT RESPONSE.BODY END
  17. // Response body echo $response->getBody(); // Read as array if

    the response was JSON $data = $response->json(); // Read as XML if the response was XML $xml = $response->xml(); 3FTQPOTFT
  18. // Get response status line values echo $response->getStatusCode(); echo $response->getReasonPhrase();

    // Read response headers echo $response->getHeader('Content-Length'); echo $response->getContentLength(); echo $response->getContentType(); foreach ($response->getHeaders() as $k => $v) { echo "{$k}: {$v}\n"; } 3FTQPOTFT
  19. $request = $client->post('/', [], [ 'Foo' => 'Baz', 'Bar' =>

    'Bam' ]); echo $request; 10453FRVFTUT
  20. $request = $client->post('/', [], [ 'Foo' => 'Baz', 'Bar' =>

    'Bam' ]); echo $request; POST / HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded User-Agent: Guzzle/3.5.0 curl/7.21.4 PHP/5.3.15 Foo=Baz&Bar=Bam 10453FRVFTUT
  21. DEFINT A-Z OPTION BASE 1 '$INCLUDE: 'GUZZLE.BI' DIM CLIENT AS

    GuzzleClient DIM REQUEST AS GuzzleRequest DIM RESPONSE AS GuzzleResponse CLIENT.BASE_URL = "http://www.amazon.com" REQUEST.METHOD = "POST" REQUEST.POST_BODY = "Foo=Baz&Bar=Bam" REQUEST.URL = "/" GuzzleSend (CLIENT, REQUEST, RESPONSE) PRINT RESPONSE.BODY END
  22. // Stream data from amazon.com $request = $client->post('http://example.com', [ 'Content-Type'

    => 'text/html', ], fopen('http://www.amazon.com', 'r')); // Stream the response to a file $request->setResponseBody('/path/to/file'); $response = $request->send(); 4USFBNJOHEBUB
  23. // Stream data from amazon.com $request = $client->post('http://example.com', [ 'Content-Type'

    => 'text/html', ], fopen('http://www.amazon.com', 'r')); // Stream the response to a file $request->setResponseBody('/path/to/file'); $response = $request->send(); 4USFBNJOHEBUB Transfer-­‐Encoding:  chunked
  24. )551FSSPST use Guzzle\Http\Exception\BadResponseException; $request = $client->get('/not_found.xml'); try { $response =

    $request->send(); } catch (BadResponseException $e) { echo $e->getMessage() . "\n"; echo $e->getRequest()->getUrl() . "\n"; echo $e->getResponse()->getRawHeaders() . "\n"; }
  25. DEFINT A-Z OPTION BASE 1 '$INCLUDE: 'GUZZLE.BI' DIM CLIENT AS

    GuzzleClient DIM REQUEST AS GuzzleRequest DIM RESPONSE AS GuzzleResponse ' Register the error handler ON ERROR GOTO Err1 CLIENT.BASE_URL = "http://www.amazon.com" REQUEST.URL = "/not_found.xml" GuzzleSend (CLIENT, REQUEST, RESPONSE) Err1: IF ERR = 404 THEN PRINT "File not found: " + REQUEST.URL END IF END
  26. use Guzzle\Common\Exception\MultiTransferException; try { $responses = $client->send([ $client->get('http://www.amazon.com/'), $client->head('http://www.google.com/'), $client->get('https://www.github.com/')

    ]); } catch (MultiTransferException $e) { foreach ($e->getFailedRequests() as $request) { echo "Failure:\n{$request}\n"; } foreach ($e->getSuccessfulRequests() as $request) { echo "Success:\n{$request}\n"; } } 1BSBMMFM3FRVFTUT
  27. 4ZNGPOZ&WFOU %JTQBUDIFS • Glue of Guzzle • Extensible through events

    • HasDispatcherInterface • Subscribers and listeners
  28. $client->getEventDispatcher()->addListener( 'request.error', function (Event $event) { if ($event['response']->getStatusCode() != 401)

    { return; } $req = $event['request']->clone(); $req->setHeader('X-Auth-Header', MyApp::getAuthToken()); $res = $req->send(); $event['request']->setResponse($res); $event->stopPropagation(); } ); $VTUPNFSSPS IBOEMJOH Event listener
  29. 1MVHJOT BLB &WFOU4VCTDSJCFST • Encapsulate the events used to modify

    a client or request • Can work across request lifecycle • Easier to share
  30. (V[[MF1MVHJOT • Async • Backoff • Cache • Cookie •

    Authentication • ErrorResponse • History • Wire logging • MD5 validation • Mock • OAuth 4ZNGPOZ&WFOU4VCTDSJCFST
  31. "OBUPNZPGB 1MVHJO • public  static  function  getSubscribedEvents() • Returns array

    mapping event names to methods • Methods should accept a Guzzle\Common\Event object
  32. use Symfony\Component\EventDispatcher\EventSubscriberInterface; class CurlAuthPlugin implements EventSubscriberInterface { private $username; private

    $password; private $scheme; public function __construct($username, $password, $scheme = CURLAUTH_BASIC) { $this->username = $username; $this->password = $password; $this->scheme = $scheme; } public static function getSubscribedEvents() { return ['client.create_request' => ['onRequestCreate', 255]]; } public function onRequestCreate(\Guzzle\Common\Event $event) { $event['request']->setAuth( $this->username, $this->password, $this->scheme ); } } CurlAuthPlugin
  33. use Guzzle\Plugin\CurlAuth\CurlAuthPlugin; // Add the auth plugin to the client

    object $authPlugin = new CurlAuthPlugin('user', 'pass'); $client->addSubscriber($authPlugin);
  34. .PTUXFC TFSWJDFDMJFOUT • Usually wrap cURL • Functions for each

    API operation • Error handling? Extensibility?
  35. .PTUXFC TFSWJDFDMJFOUT • Usually wrap cURL • Functions for each

    API operation • Error handling? Extensibility? • Are written in a bubble
  36. .PTUXFC TFSWJDFDMJFOUT • Usually wrap cURL (poorly) • Functions for

    each API operation • Error handling? Extensibility? • Are written in a bubble
  37. (V[[MFa4FSWJDFa$MJFOU • Extends from Guzzle\Http\Client • Creates and executes Command

    objects • Owns a Service Description • Created by a Service Builder
  38. $PNNBOET • Encapsulate actions of an API • Input validation

    • Request serialization • Response parsing
  39. $PODSFUF $PNNBOET • Actual PHP class . ├──  Commands │

         └──  Foo.php └──  MyClient.php
  40. $PODSFUF $PNNBOET • Actual PHP class • Low level .

    ├──  Commands │      └──  Foo.php └──  MyClient.php
  41. $PODSFUF $PNNBOET • Actual PHP class • Low level •

    Tight coupling to an API . ├──  Commands │      └──  Foo.php └──  MyClient.php
  42. 4FSWJDF EFTDSJQUJPOT • Define an API using a JSON document

    • Defines inputs and outputs • Versioned descriptions are swappable
  43. 4FSWJDF EFTDSJQUJPOT • Define an API using a JSON document

    • Defines inputs and outputs • Versioned descriptions are swappable • API docs? Other language clients?
  44. { "operations": { "operationName": { "extends": "string", "httpMethod": "GET|POST|PUT|DELETE|PATCH|string", "uri":

    "string", "summary": "string", "class": "string", "responseClass": "string", "responseNotes": { "type": "string", "description": "string", "responseType": "primitive|class|model|etc", "deprecated": false, "errorResponses": [ { "code": 500, "phrase": "Unexpected Error", "class": "string" } ] "parameters": {} } } }
  45. 1BSBNFUFST • Follow JSON schema • Types: object, array, string,

    number, etc • Required / default values • Validation • locations: header, query, uri, body, etc...
  46. • Rewrote the AWS SDK for PHP on Guzzle •

    Support 31 services w/ multiple versions • Only possible because of service descriptions (V[[MFBOE"84
  47. 'PutObject': { 'httpMethod': 'PUT', 'uri': '/{Bucket}{/Key*}', 'responseClass': 'PutObjectOutput', 'parameters': {

    'Bucket': { 'required': true, 'type': 'string', 'location': 'uri', }, 'Key': { 'required': true, 'type': 'string', 'location': 'uri' }, 'Body': { 'type': [ 'string', 'object' ], 'location': 'body', }, 'ContentType': { 'type': 'string', 'location': 'header', 'sentAs': 'Content-Type', } } } $client->putObject([ 'Bucket' => 'foo', 'Key' => 'baz', 'Body' => 'bar' ]); Input Request GET  /foo/baz  HTTP/1.1 Host:  s3.amazonaws.com Content-­‐Length:  3 bar
  48. 'PutObject': { 'httpMethod': 'PUT', 'uri': '/{Bucket}{/Key*}', 'responseClass': 'PutObjectOutput', 'parameters': {

    'Bucket': { 'required': true, 'type': 'string', 'location': 'uri', }, 'Key': { 'required': true, 'type': 'string', 'location': 'uri' }, 'Body': { 'type': [ 'string', 'object' ], 'location': 'body', }, 'ContentType': { 'type': 'string', 'location': 'header', 'sentAs': 'Content-Type', } } } $client->putObject([ 'Bucket' => 'foo', 'Key' => 'baz', 'Body' => 'bar' ]); Input Request GET  /foo/baz  HTTP/1.1 Host:  s3.amazonaws.com Content-­‐Length:  3 bar
  49. 'PutObject': { 'httpMethod': 'PUT', 'uri': '/{Bucket}{/Key*}', 'responseClass': 'PutObjectOutput', 'parameters': {

    'Bucket': { 'required': true, 'type': 'string', 'location': 'uri', }, 'Key': { 'required': true, 'type': 'string', 'location': 'uri' }, 'Body': { 'type': [ 'string', 'object' ], 'location': 'body', }, 'ContentType': { 'type': 'string', 'location': 'header', 'sentAs': 'Content-Type', } } } $client->putObject([ 'Bucket' => 'foo', 'Key' => 'baz', 'Body' => 'bar' ]); Input Request GET  /foo/baz  HTTP/1.1 Host:  s3.amazonaws.com Content-­‐Length:  3 bar
  50. 'PutObject': { 'httpMethod': 'PUT', 'uri': '/{Bucket}{/Key*}', 'responseClass': 'PutObjectOutput', 'parameters': {

    'Bucket': { 'required': true, 'type': 'string', 'location': 'uri', }, 'Key': { 'required': true, 'type': 'string', 'location': 'uri' }, 'Body': { 'type': [ 'string', 'object' ], 'location': 'body', }, 'ContentType': { 'type': 'string', 'location': 'header', 'sentAs': 'Content-Type', } } } $client->putObject([ 'Bucket' => 'foo', 'Key' => 'baz', 'Body' => 'bar' ]); Input Request GET  /foo/baz  HTTP/1.1 Host:  s3.amazonaws.com Content-­‐Length:  3 bar
  51. 'PutObjectOutput': { 'type': 'object', 'properties': { 'Expiration': { 'type': 'string',

    'location': 'header', 'sentAs': 'x-amz-expiration', }, 'ETag': { 'type': 'string', 'location': 'header', }, 'VersionId': { 'type': 'string', 'location': 'header', 'sentAs': 'x-amz-version-id', }, 'RequestId': { 'type': 'string', 'location': 'header', 'sentAs': 'x-amz-request-id', } } } Response Model HTTP/1.1  200  OK x-­‐amz-­‐id-­‐2:  1234 x-­‐amz-­‐request-­‐id:  5490 Date:  Thu,  16  May  2013  00:01:48  GMT ETag:  "3a4f009649c38ef10c..." Content-­‐Length:  0 Server:  AmazonS3 print_r($result->toArray()); Array ( [Expiration] => [ETag] => "3a4f009649c38ef10c..." [VersionId] => [RequestId] => "5490" )
  52. 'PutObjectOutput': { 'type': 'object', 'properties': { 'Expiration': { 'type': 'string',

    'location': 'header', 'sentAs': 'x-amz-expiration', }, 'ETag': { 'type': 'string', 'location': 'header', }, 'VersionId': { 'type': 'string', 'location': 'header', 'sentAs': 'x-amz-version-id', }, 'RequestId': { 'type': 'string', 'location': 'header', 'sentAs': 'x-amz-request-id', } } } Response Model HTTP/1.1  200  OK x-­‐amz-­‐id-­‐2:  1234 x-­‐amz-­‐request-­‐id:  5490 Date:  Thu,  16  May  2013  00:01:48  GMT ETag:  "3a4f009649c38ef10c..." Content-­‐Length:  0 Server:  AmazonS3 print_r($result->toArray()); Array ( [Expiration] => [ETag] => "3a4f009649c38ef10c..." [VersionId] => [RequestId] => "5490" )
  53. 'PutObjectOutput': { 'type': 'object', 'properties': { 'Expiration': { 'type': 'string',

    'location': 'header', 'sentAs': 'x-amz-expiration', }, 'ETag': { 'type': 'string', 'location': 'header', }, 'VersionId': { 'type': 'string', 'location': 'header', 'sentAs': 'x-amz-version-id', }, 'RequestId': { 'type': 'string', 'location': 'header', 'sentAs': 'x-amz-request-id', } } } Response Model HTTP/1.1  200  OK x-­‐amz-­‐id-­‐2:  1234 x-­‐amz-­‐request-­‐id:  5490 Date:  Thu,  16  May  2013  00:01:48  GMT ETag:  "3a4f009649c38ef10c..." Content-­‐Length:  0 Server:  AmazonS3 print_r($result->toArray()); Array ( [Expiration] => [ETag] => "3a4f009649c38ef10c..." [VersionId] => [RequestId] => "5490" )
  54. { "name": "Twitter", "apiVersion": "1.1", "baseUrl": "https://api.twitter.com/1.1", "operations": { "GetMentions":

    { "httpMethod": "GET", "uri": "statuses/mentions_timeline.json", "responseClass": "GetMentionsOutput", "additionalParameters": { "location": "query" } } }, "models": { "GetMentionsOutput": { "type": "object", "additionalProperties": { "location": "json" } } } }
  55. use Guzzle\Service\Description\ServiceDescription as S; // Set the description on the

    client $description = S::factory('/path/client.json'); $client->setDescription($description);
  56. 8IBUBCPVU 0"VUI use Guzzle\Plugin\Oauth\OauthPlugin; $plugin = new OauthPlugin([ 'consumer_key' =>

    '***', 'consumer_secret' => '***', 'token' => '***', 'token_secret' => '***' ]); // Sign all requests with the OauthPlugin $client->addSubscriber($plugin);
  57. // Create and execute the GetMentions operation $model = $client->getMentions(['count'

    => 3]); // The “model” is a Guzzle Model object echo $model['status'];
  58. use Guzzle\Common\Collection; use Guzzle\Plugin\Oauth\OauthPlugin; use Guzzle\Service\Description\ServiceDescription; class TwitterClient extends Guzzle\Service\Client

    { public static function factory($config = []) { $required = [ 'consumer_key', 'consumer_secret', 'token', 'token_secret' ]; // Merge in default settings and validate the config $config = Collection::fromConfig($config, [], $required); // Create a new Twitter client $client = new self(); $client->setConfing($config); $client->addSubscriber(new OauthPlugin($config->toArray())); return $client; } }
  59. $twitter = TwitterClient::factory([ 'consumer_key' => '****', 'consumer_secret' => '****', 'token'

    => '****', 'token_secret' => '****' ]); $model = $client->getMentions(['count' => 3]);