Skip to content

[7.0] ZTTP#31449

Merged
taylorotwell merged 32 commits intomasterfrom
http
Feb 12, 2020
Merged

[7.0] ZTTP#31449
taylorotwell merged 32 commits intomasterfrom
http

Conversation

@taylorotwell
Copy link
Copy Markdown
Member

@taylorotwell taylorotwell commented Feb 12, 2020

This is a port of @adamwathan's ZTTP Guzzle convenience layer. It provides much nicer syntax for the 90% use case of Guzzle where you just need to POST some JSON to an endpoint. In addition, this supplements ZTTP with stubbing / faking inspired by @jasonmccreary's work on that library.

This is not an entirely new client - it is only a UX / DX convenience layer on top of Guzzle. We will not be adding a lot of complicated features to this. If you need very, very robust or complicated logic just use Guzzle directly. We aren't trying to expose every feature of Guzzle through this API.

Basic Usage Overview

use Illuminate\Support\Facades\Http;

$response = Http::post('url', [
    'name' => 'Taylor',
]);

echo $response['foo'];

$response->body()
$response->json()
$response->status()
$response->ok()
$response->successful() (>= 200 && < 300)
$response->serverError()
$response->clientError()

By default, the content type is application/json. You may also easily send form-urlencoded requests:

$response = Http::asForm()->post('url', [
    'name' => 'Taylor',
]);

You may also send files as multi-part requests:

$response = Http::attach('name', 'stream/contents', 'filename.txt')->post('url');

Headers / Authentication

Headers may be added using the withHeaders method:

$response = Http::withHeaders(['X-Foo' => 'bar'])->post('url', [
    'name' => 'Taylor',
]);

Authorization / bearer tokens...

$response = Http::withToken('token')->post('url', [
    'name' => 'Taylor',
]);

Basic authentication...

$response = Http::withBasicAuth('username', 'password')->post('url', [
    'name' => 'Taylor',
]);

Errors

By default, like ZTTP, exceptions are not thrown on client and server errors. If you receive a response that is not successful and you would like to throw an exception, you may call $response->throw():

$response = Http::post(...);

if (! $response->successful()) {
    $response->throw();
}

Testing / Faking

Like some other services in Laravel, the HTTP client supports faking. When calling Http::fake() without any additional arguments, all responses will be empty 200 level responses:

Http::fake();

$response = Http::post('url');

However, you may stub the responses returned by specific endpoints by passing an array to the fake method, any requests that are not to URLs matching the given pattern will actually be executed. In this example, I will use the Http::response method to quickly generate a stub response that should be returned.

Http::fake([
    'github.com/*' => Http::response([1, 2, 3], 200, ['Headers']),
]);

A final fallback route could also be provided:

Http::fake([
    'github.com/*' => Http::response([1, 2, 3], 200, ['Headers']),
    '*' => Http::response('', 200),
]);

In addition, you could specify a sequence of responses to be returned. These responses will be returned in order as the endpoint is called:

Http::fake([
    '*' => Http::sequence([Http::response('foo', 200), Http::response('bar', 200)]),
]);

Finally, for maximum flexibility you may pass a Closure to Http::fake which can examine the request, perform conditional logic, and return an appropriate response:

Http::fake(function ($request) {
    return Http::response('foo', 200);
});

@GrahamCampbell GrahamCampbell changed the title ZTTP [7.0] ZTTP Feb 12, 2020
@SjorsO
Copy link
Copy Markdown
Contributor

SjorsO commented Feb 12, 2020

First of all, big fan of this PR 👍

However, it might be worthwhile to take some inspiration from Sjorso/Gobble, specifically how easy Gobble makes it to push a sequence of responses:

Gobble::fake()
    ->pushJson(['fact' => 'Cats are great!'])    
    ->pushFile('tests/Fixtures/huge-google-maps-api-response.json')
    ->pushEmptyResponse(403);

// vs

Http::fake([
    '*' => Http::sequence([
        Http::response(['fact' => 'Cats are great!'], 200),
        Http::response([file_get_contents('tests/Fixtures/huge-google-maps-api-response.json'), 200),
        Http::response('', 403),
    ]),
]);

Gobble uses the normal Guzzle way of mocking responses, by using a mock handler filled with a sequence of responses. If a Guzzle call is made, but the mock handler is empty, an OutOfBoundsException: Mock queue is empty exception is thrown. Since you should almost always know how many calls your test is going to do, this seems to me like a better default than always returning 200. For those few situations where you don't know (or don't care) how many calls you are going to do, Gobble has an autofillResponseStack() method.

Also, when Gobble records responses, it wraps them in a RequestHistory object that provides useful assertions (much like the TestResponse in feature tests). For example, this makes it easy to check if the request you sent had correctly set headers.

Either way, I'll most likely be deprecating Gobble when 7.0 is released, and re-write my tests to use this new built-in HTTP client instead.

@jasonmccreary
Copy link
Copy Markdown
Contributor

jasonmccreary commented Feb 12, 2020

@SjorsO there may be opportunity down the road to add some syntactic sugar to streamline the stubbing of specific requests.

As noted in the PR description, fake also accepts a callback where you may set up all the logic you would ever need for request/response verification.

@SjorsO
Copy link
Copy Markdown
Contributor

SjorsO commented Feb 12, 2020

@SjorsO there may be opportunity down the road to add some syntactic sugar to streamline the stubbing of specific requests.

I've been using Gobble for the past two years on many client and personal projects, so I am definitely biased, but I feel like the Gobble way of mocking responses is a better developer experience and is worth considering before this PR is merged.

@taylorotwell
Copy link
Copy Markdown
Member Author

@SjorsO once this is merged you could probably pretty easily PR so that Http::sequence() without arguments returns some kind of SequenceBuilder that you can chain onto. I like your syntax overall.

Copy link
Copy Markdown

@ashokkasti ashokkasti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants