Often in our applications we need to make HTTP requests to external services either to integrate with an API, upload a file to S3, or perform some other action that requires us to interface over HTTP. PHP doesn’t natively make these requests user friendly so many of us turn to the 3rd-party library Guzzle. But how well do you know Guzzle beyond the basics?
Guzzle is much more than syntactic sugar for HTTP requests. PHP is a synchronous language by nature, but did you know that you can make asynchronous HTTP requests out of the box with Guzzle? It’s functionality extends even further with support for custom request handlers and middleware.
In this talk we’ll start with some basic example requests using Guzzle. Then we’ll look at extending our requests with middleware such as logging, retry middleware, handling failed requests, and mapping responses from 3rd-party APIs. From there, we’ll refactor our synchronous requests to be asynchronous using promises and generators to process our async responses. Finally, we’ll look at how to test our requests without needing to send them over the internet. You’ll leave this presentation with a deeper understanding of the power of Guzzle, how to extend it with your own middleware, how to speed up your application with async requests and how to test your requests without external dependencies.
2. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
About me
Steven Wade
• Husband, father
• Founder/Organizer of UpstatePHP
• Engineer at Follow Up Boss
Twitter: @stevenwadejr
Email: stevenwadejr@gmail.com
3. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Austin, we have a problem
4. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
5. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
6. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
7. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
• Headers
8. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
• Headers
• GET, POST, PUT, DELETE, ...
9. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
• Headers
• GET, POST, PUT, DELETE, ...
• Status codes
10. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
• Headers
• GET, POST, PUT, DELETE, ...
• Status codes
• Sending a request
11. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
• Headers
• GET, POST, PUT, DELETE, ...
• Status codes
• Sending a request
• Parsing a response
12. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
HTTP Requests are Hard
• URLs
• Headers
• GET, POST, PUT, DELETE, ...
• Status codes
• Sending a request
• Parsing a response
• Handling errors
23. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
cURL
✓ Supports persistent connections
✓ Supports parallel requests
✓ Most common way of making external requests
24. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
cURL
✓ Supports persistent connections
✓ Supports parallel requests
✓ Most common way of making external requests
๏ Not built-in: requires library and extension
25. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
cURL
✓ Supports persistent connections
✓ Supports parallel requests
✓ Most common way of making external requests
๏ Not built-in: requires library and extension
๏ Clunky and confusing API
26. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
cURL
✓ Supports persistent connections
✓ Supports parallel requests
✓ Most common way of making external requests
๏ Not built-in: requires library and extension
๏ Clunky and confusing API
๏ Cumbersome error handling
30. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
31. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
32. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
33. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
34. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
• Understand the basics of making requests with Guzzle
35. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
• Understand the basics of making requests with Guzzle
• Understand the concept of middleware and how to use it
within your requests
36. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
• Understand the basics of making requests with Guzzle
• Understand the concept of middleware and how to use it
within your requests
• Be able to make asynchronous requests
37. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
• Understand the basics of making requests with Guzzle
• Understand the concept of middleware and how to use it
within your requests
• Be able to make asynchronous requests
• Know how to test your HTTP requests
38. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
39. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
GUZZLE
[angelic singing]
40. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is Guzzle?
"Guzzle is a PHP HTTP client that makes it easy to send HTTP requests
and trivial to integrate with web services."
http://docs.guzzlephp.org
41. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Guzzle Provides:
•Simple interface for building query strings, POST requests, uploading JSON data, etc...
•Can send both synchronous and asynchronous requests using the same interface.
•Uses PSR-7 interfaces for requests, responses, and streams.
•Abstracts away the underlying HTTP transport (can use streams, cURL, other).
•Middleware system allows you to augment and compose client behavior.
42. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Installing Guzzle
composer require guzzlehttp/guzzle
43. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Basic Request with Guzzle
$client = new GuzzleHttpClient;
$response = $client->get('http://example.com/foo');
44. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7
45. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
"Describes common interfaces for representing HTTP messages as
described in RFC 7230 and RFC 7231, and URIs for use with HTTP
messages as described in RFC 3986."
46. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Huh?
47. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Huh?
48. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
49. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
Request
Method
50. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
Request
Method
Request
Target
51. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
Request
Method
Request
Target
Protocol
Version
52. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
Headers
Request
Method
Request
Target
Protocol
Version
53. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is PSR-7?
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=batBody
Headers
Request
Method
Request
Target
Protocol
Version
55. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
56. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
57. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
58. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
59. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
60. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
61. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
Via a bridge:
62. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
Via a bridge:
• Laravel
63. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
Via a bridge:
• Laravel
• Symfony
64. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
Via a bridge:
• Laravel
• Symfony
• Standard representation of
HTTP requests and responses
65. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
PSR-7 - Why?
Who uses it? Why use it?
By default:
• Guzzle
• LeagueRoute
• Slim Framework
Via a bridge:
• Laravel
• Symfony
• Standard representation of
HTTP requests and responses
• Useful in an API proxy/gateway
and combined with a PSR-7
router
66. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware
67. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is middleware?
68. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is middleware?
Diagram stolen from Slim Framework showing how HTTP middleware layers wrap an application.
69. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is middleware?
Diagram stolen from Slim Framework showing how HTTP middleware layers wrap an application.
Request
Response
cURL
70. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware
Examples
71. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Defaults
74. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Defaults
• HTTP Errors
• Redirect
• Cookies
75. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Defaults
• HTTP Errors
• Redirect
• Cookies
• Prepare Body
76. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Defaults
• HTTP Errors
• Redirect
• Cookies
• Prepare Body
• HandlerStack::create()
77. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Tap
GuzzleHttpMiddleware::tap($before, $after);
78. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Tap
use LeagueStatsDClient as StatsD;
$stack = GuzzleHttpHandlerStack::create();
$statsd = new StatsD;
$statsd->configure([...]);
$stack->push(GuzzleHttpMiddleware::tap(function () use ($statsd) {
$statsd->increment('apikey.usage');
}));
80. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Tap
$stack->push(Middleware::tap(app()->make(ApiKeyUsage::class)));
class ApiKeyUsage {
private $statsd;
public function __construct(StatsD $statsd) {
$this->statsd = $statsd;
}
public function __invoke(RequestInterface $request, array $options) {
$this->statsd->increment('apikey.usage');
}
}
81. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Log
GuzzleHttpMiddleware::log($logger, $messageFormatter);
82. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Log
$logger = new MonologLogger('http');
$logger->pushHandler(
new MonologHandlerStreamHandler(
'path/to/your.log',
MonologLogger::INFO
)
);
$stack = GuzzleHttpHandlerStack::create();
$stack->push(GuzzleHttpMiddleware::log(
$logger,
new GuzzleHttpMessageFormatter
));
83. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Log
$logger = new MonologLogger('http');
$logger->pushHandler(
new MonologHandlerStreamHandler(
'path/to/your.log',
MonologLogger::INFO
)
);
$stack = GuzzleHttpHandlerStack::create();
$stack->push(GuzzleHttpMiddleware::log(
$logger,
new GuzzleHttpMessageFormatter
));
84. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Log
$logger = new MonologLogger('http');
$logger->pushHandler(
new MonologHandlerStreamHandler(
'path/to/your.log',
MonologLogger::INFO
)
);
$stack = GuzzleHttpHandlerStack::create();
$stack->push(GuzzleHttpMiddleware::log(
$logger,
new GuzzleHttpMessageFormatter
));
85. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Log
$logger = new MonologLogger('http');
$logger->pushHandler(
new MonologHandlerStreamHandler(
'path/to/your.log',
MonologLogger::INFO
)
);
$stack = GuzzleHttpHandlerStack::create();
$stack->push(GuzzleHttpMiddleware::log(
$logger,
new GuzzleHttpMessageFormatter
));
86. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Log
GuzzleHttpMiddleware::log($logger, $messageFormatter);
87. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Middleware - Retry
GuzzleHttpMiddleware::retry($decider, $delay);
115. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
JsonStream
use GuzzleHttpPsr7StreamDecoratorTrait;
use PsrHttpMessageStreamInterface;
class JsonStream implements StreamInterface {
use StreamDecoratorTrait;
public function json(): ?array {
return json_decode((string) $this->getContents(), true);
}
}
116. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
JsonStream
$stack = HandlerStack::create();
$stack->push(Middleware::mapResponse(
function (ResponseInterface $response) {
$jsonStream = new JsonStream($response->getBody());
return $response->withBody($jsonStream);
}
));
124. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Concurrent Requests
125. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Concurrent Requests
126. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
How do we send async requests?
$client->send($request)
127. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
How do we send async requests?
$client->send($request)
$client->sendAsync($request)
128. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is a Promise?
129. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is a Promise?
130. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
What is a Promise?
"A Promise is an object representing the eventual
result of an asynchronous operation"
https://amphp.org/amp/promises/
141. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
GuzzleHttpPromisefunctions
142. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
GuzzleHttpPromisefunctions
// Waits on all of the provided promises
// and returns the fulfilled values.
unwrap($promises);
143. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
GuzzleHttpPromisefunctions
// Waits on all of the provided promises
// and returns the fulfilled values.
unwrap($promises);
// Returns a promise that is fulfilled when
// all the items in the array are fulfilled.
all($promises);
144. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
GuzzleHttpPromisefunctions
// Waits on all of the provided promises
// and returns the fulfilled values.
unwrap($promises);
// Returns a promise that is fulfilled when
// all the items in the array are fulfilled.
all($promises);
// Returns a promise that is fulfilled when
// all of the provided promises have
// been fulfilled or rejected.
settle($promises);
145. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Coroutines
"Coroutines are a general control structure whereby flow control is
cooperatively passed between two different routines without returning."
- user21714
https://stackoverflow.com/questions/553704/what-is-a-coroutine
146. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Coroutines
"Coroutines are interruptible functions"
https://amphp.org/amp/coroutines/
148. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Dashboard - Coroutines
new GuzzleHttpPromiseCoroutine(
function() use ($client, $endpoint, $headers) {
$request = new Request('GET', $endpoint, $headers);
$response = yield $client->sendAsync($request);
$json = $response->getBody()->json();
yield $json[$endpoint] ?? [];
}
);
149. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Dashboard - Coroutines
new GuzzleHttpPromiseCoroutine(
function() use ($client, $endpoint, $headers) {
$request = new Request('GET', $endpoint, $headers);
$response = yield $client->sendAsync($request);
$json = $response->getBody()->json();
yield $json[$endpoint] ?? [];
}
);
150. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Dashboard - Coroutines
new GuzzleHttpPromiseCoroutine(
function() use ($client, $endpoint, $headers) {
$request = new Request('GET', $endpoint, $headers);
$response = yield $client->sendAsync($request);
$json = $response->getBody()->json();
yield $json[$endpoint] ?? [];
}
);
151. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Dashboard - Coroutines
new GuzzleHttpPromiseCoroutine(
function() use ($client, $endpoint, $headers) {
$request = new Request('GET', $endpoint, $headers);
$response = yield $client->sendAsync($request);
$json = $response->getBody()->json();
yield $json[$endpoint] ?? [];
}
);
152. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Why use coroutines?
153. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Why use coroutines?
• Avoid multiple/chained callbacks
154. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Why use coroutines?
• Avoid multiple/chained callbacks
• Code flows more naturally
155. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Why use coroutines?
• Avoid multiple/chained callbacks
• Code flows more naturally
• Use try/catch to handle errors
156. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Testing
157. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Mock Handler
// Create a mock and queue two responses.
$mock = new GuzzleHttpHandlerMockHandler([
new Response(200, ['X-Foo' => 'Bar']),
new Response(202, ['Content-Length' => 0]),
new RequestException('Server error', new Request('GET', 'test'))
]);
$handler = GuzzleHttpHandlerStack::create($mock);
$client = new GuzzleHttpClient(['handler' => $handler]);
// The first request is intercepted with the first response.
echo $client->request('GET', '/')->getStatusCode(); //> 200
// The second request is intercepted with the second response.
echo $client->request('GET', '/')->getStatusCode(); //> 202
158. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
History Middleware
$container = [];
$history = Middleware::history($container);
$mockHandler = new GuzzleHttpHandlerMockHandler([
new Response(200)
]);
$stack = HandlerStack::create($mockHandler);
$stack->push($history);
$client = new Client([
'base_uri' => $baseUrl,
'handler' => $stack
]);
159. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
History Middleware
$client->request('GET', 'people', ['headers' => $headers]);
foreach ($container as $transaction) {
echo $transaction['request']->getMethod();
if ($transaction['response']) {
echo $transaction['response']->getStatusCode();
} elseif ($transaction['error']) {
echo $transaction['error'];
}
}
160. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
History Middleware
$this->assertEquals(
'Swade-Co',
$transaction['request']->getHeader('X-System')[0]
);
161. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
162. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
163. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
164. Steven Wade - @stevenwadejrhttps://joind.in/talk/41978
Comments/Questions/Concerns/Hate Mail
Twitter: @stevenwadejr
Email: stevenwadejr@gmail.com
https://joind.in/talk/41978