Guzzle - SymfonyLive Portland

Guzzle - SymfonyLive Portland

1e1ceeb39866031496170d87cae919ef?s=128

Michael Dowling

June 13, 2013
Tweet

Transcript

  1. #VJMEJOHXFCTFSWJDF DMJFOUTXJUI (V[[MF Michael Dowling http://guzzlephp.org https://github.com/guzzle/guzzle

  2. !NUEPXMJOH http://mtdowling.com https://github.com/mtdowling Amazon Web Services AWS SDK for PHP

    Author of Guzzle *`N.JDIBFM%PXMJOH
  3. Part One 1)1)551$MJFOUT

  4. IUUQTUSFBN XSBQQFS

  5. IUUQTUSFBN XSBQQFS • Easy to send simple GET requests •

    Built-in to PHP
  6. 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
  7. <?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
  8. // 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
  9. D63- • Fast • Supports HTTP/1.1 • Supports streaming requests

    & responses • Can do pretty much anything • Almost everyone uses it • Very cumbersome API
  10. 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);
  11. 1&$-)551 • Supports HTTP/1.1 • Supports streaming requests • Does

    not stream responses • Requires uncommon extension
  12. 4PDLFUT • More control • Lots of effort because... •RFC

    2616 is complicated e.g. fsockopen
  13. 1)1$MJFOUT • Guzzle • Buzz • Zend/Http • Requests •

    ...
  14. (V[[MF • Supports HTTP/1.1 • Parallel & persistent requests •

    Streaming requests and responses • Extensible with plugins • Web service framework
  15. IT REQUIRES CURL!

  16. D63-BTB EFQFOEFODZ

  17. D63-BTB EFQFOEFODZ • Popular libs require cURL • AWS, Magento,

    Facebook, Google, ...
  18. D63-BTB EFQFOEFODZ • Popular libs require cURL • AWS, Magento,

    Facebook, Google, ... • Most shared hosts include it • GoDaddy, DreamHost, ServerGrove...
  19. 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/
  20. *OTUBMMJOH(V[[MF { "require": { "guzzle/guzzle": "3.5.*" } } composer.json composer.phar

    install
  21. IT’S NOT LIGHTWEIGHT!

  22. (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.*" } }
  23. • AWS SDK for PHP • Drupal 8 • Goutte

    • Tumblr (V[[MFJOUIF8JME
  24. • 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
  25. None
  26. • Clients are the glue • Clients create and send

    Requests • Base URLs (V[[MFa)UUQa$MJFOU
  27. <?php use Guzzle\Http\Client; $client = new Client('https://api.twitter.com/1.1'); $request = $client->get('statuses/user_timeline.json');

    $response = $request->send(); $MJFOU#BTF63-T RFC  3986
  28. 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
  29. $client = new Guzzle\Http\Client(); $url = 'http://www.amazon.com?foo=bar'; $response = $client->get($url)->send();

    4JNQMF(&5 SFRVFTU
  30. $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
  31. 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
  32. // 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
  33. // 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
  34. $request = $client->post('/', [], [ 'Foo' => 'Baz', 'Bar' =>

    'Bam' ]); echo $request; 10453FRVFTUT
  35. $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
  36. 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
  37. // 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
  38. // 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
  39. &SSPS)BOEMJOH

  40. &SSPS)BOEMJOH •Application / Protocol errors • HTTP status-code >=400,<=599

  41. &SSPS)BOEMJOH •Application / Protocol errors • HTTP status-code >=400,<=599 •Transport

    / Network errors • cURL errors
  42. )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"; }
  43. 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
  44. 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
  45. None
  46. None
  47. None
  48. 4ZNGPOZ&WFOU %JTQBUDIFS

  49. 4ZNGPOZ&WFOU %JTQBUDIFS • Glue of Guzzle

  50. 4ZNGPOZ&WFOU %JTQBUDIFS • Glue of Guzzle • Extensible through events

  51. 4ZNGPOZ&WFOU %JTQBUDIFS • Glue of Guzzle • Extensible through events

    • HasDispatcherInterface
  52. 4ZNGPOZ&WFOU %JTQBUDIFS • Glue of Guzzle • Extensible through events

    • HasDispatcherInterface • Subscribers and listeners
  53. $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
  54. 1MVHJOT BLB &WFOU4VCTDSJCFST • Encapsulate the events used to modify

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

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

    mapping event names to methods • Methods should accept a Guzzle\Common\Event object
  57. 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
  58. use Guzzle\Plugin\CurlAuth\CurlAuthPlugin; // Add the auth plugin to the client

    object $authPlugin = new CurlAuthPlugin('user', 'pass'); $client->addSubscriber($authPlugin);
  59. Part Two 8FCTFSWJDFDMJFOUT

  60. 8IBUEPFTBXFC TFSWJDFDMJFOUEP

  61. .PTUXFC TFSWJDFDMJFOUT

  62. .PTUXFC TFSWJDFDMJFOUT • Usually wrap cURL

  63. .PTUXFC TFSWJDFDMJFOUT • Usually wrap cURL • Functions for each

    API operation
  64. .PTUXFC TFSWJDFDMJFOUT • Usually wrap cURL • Functions for each

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

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

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

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

    • Request serialization • Response parsing
  69. $PODSFUF $PNNBOET

  70. $PODSFUF $PNNBOET • Actual PHP class . ├──  Commands │

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

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

    Tight coupling to an API . ├──  Commands │      └──  Foo.php └──  MyClient.php
  73. 0QFSBUJPO $PNNBOET • Powered by a Service Description • Requires

    no PHP code • High level
  74. Part Three %FpOJOH"1*T

  75. 4FSWJDF EFTDSJQUJPOT

  76. 4FSWJDF EFTDSJQUJPOT • Define an API using a JSON document

  77. 4FSWJDF EFTDSJQUJPOT • Define an API using a JSON document

    • Defines inputs and outputs
  78. 4FSWJDF EFTDSJQUJPOT • Define an API using a JSON document

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

    • Defines inputs and outputs • Versioned descriptions are swappable • API docs? Other language clients?
  80. { "name": "string", "apiVersion": "string|number", "baseUrl": "string", "description": "string", "operations":

    {}, "models": {}, "includes": ["string.php", "string.json"] }
  81. { "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": {} } } }
  82. 1BSBNFUFST • Follow JSON schema • Types: object, array, string,

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

    Support 31 services w/ multiple versions • Only possible because of service descriptions (V[[MFBOE"84
  84. '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
  85. '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
  86. '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
  87. '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
  88. '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" )
  89. '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" )
  90. '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" )
  91. 5XJUUFSDMJFOU

  92. None
  93. { "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" } } } }
  94. use Guzzle\Service\Description\ServiceDescription as S; // Set the description on the

    client $description = S::factory('/path/client.json'); $client->setDescription($description);
  95. 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);
  96. // Create and execute the GetMentions operation $model = $client->getMentions(['count'

    => 3]); // The “model” is a Guzzle Model object echo $model['status'];
  97. None
  98. $MJFOU'BDUPSZ

  99. $MJFOU'BDUPSZ • Encapsulates bootstrapping of client

  100. $MJFOU'BDUPSZ • Encapsulates bootstrapping of client • Provides input validation

  101. $MJFOU'BDUPSZ • Encapsulates bootstrapping of client • Provides input validation

    • Used with a ServiceBuilder
  102. 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; } }
  103. $twitter = TwitterClient::factory([ 'consumer_key' => '****', 'consumer_secret' => '****', 'token'

    => '****', 'token_secret' => '****' ]); $model = $client->getMentions(['count' => 3]);
  104. 0UIFSGFBUVSFT • Service builder configs • Resource iterators • Batching

    abstractions
  105. 'VUVSFPG(V[[MF • More tooling • OAuth2 Plugin • Simplified procedural

    API • What would you like to see?
  106. #VJMEJOHXFCTFSWJDF DMJFOUTXJUI (V[[MF @mtdowling

  107. #VJMEJOHXFCTFSWJDF DMJFOUTXJUI (V[[MF @mtdowling Any questions?