mtdowling

Guzzle 4 Release Candidate

After nearly 8 months of development, I’m happy to announce a Guzzle 4 release candidate. Guzzle 4 is a huge step forward for the project and brings with it a number of improvements over previous versions.

Swappable HTTP adapters

Guzzle no longer requires cURL. New in Guzzle 4 is the ability to swap out the HTTP adapter used to send requests over the wire. You can now, for example, create a custom HTTP adapter to send requests using sockets, create an adapter to send requests with React, or create adapter proxies to choose the most appropriate adapter to use for a request based on its configuration options.

Guzzle actually automatically determines which adapter to use based on your environment and the request options of a request. When cURL is available on your system, Guzzle will automatically use cURL. When a request is sent with the stream=true request option, Guzzle will automatically use the PHP stream wrapper HTTP adapter so that bytes are only read from the HTTP stream as needed.

Guzzle has historically only utilized cURL to send HTTP requests. cURL is an amazing HTTP client (arguably the best), and Guzzle will continue to use it by default when it is available. It is rare, but some developers don’t have cURL installed on their systems or run into version specific issues. By allowing swappable HTTP adapters, Guzzle is now much more customizable and able to adapt to fit the needs of more developers.

These swappable HTTP adapters make it super easy to work with streaming API’s like Twitter’s:

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\Oauth\Oauth1;

$client = new Client([
    'base_url' => 'https://stream.twitter.com/1/',
    'defaults' => ['auth' => 'oauth']
]);

$client->getEmitter()->attach(new Oauth1([
    'consumer_key'    => '***',
    'consumer_secret' => '***',
    'token'           => '***',
    'token_secret'    => '***'
]));

$response = $client->post('statuses/filter.json', [
    'body'   => ['track' => 'bieber'],
    'stream' => true
]);

$body = $response->getBody();

while (!$body->eof()) {
    $line = $body::readLine($body);
    $data = json_decode($line, true);
    var_export($data);
}

Requires PHP 5.4 or Greater

Due to it’s use of traits and array dereferencing, Guzzle 4 now requires PHP 5.4 or greater. PHP 5.4 is becoming more and more common as a requirement for PHP libraries. Guzzle now joins libraries like Laravel, Drupal 8, and React with its PHP 5.4 requirement.

Improved Performance

By removing superfluous features and better organizing how things work together, Guzzle 4 now offers a significant performance increase over previous version. Based on some rudimentary tests, Guzzle 4 is 40-50% faster for sending requests one after the other, and about 25% faster at sending requests in parallel.

When running PHP 5.5 or greater, Guzzle will now automatically send serial requests using cURL easy handles rather than cURL multi handles. This makes the typical use case of sending a few requests serially considerably faster, consumes less CPU, and works around various issues where PHP’s cURL bindings poorly interface with cURL’s multi API (e.g., timeouts, having to convert curl handles to file descriptors when curl_multi_select() is called, etc…).

Simpler Interfaces

The public interface of almost every class in Guzzle has been thoroughly analyzed to better define a minimal public API (following the “when in doubt, leave it out” mantra). Trimming down convenience methods, making class properties and methods private instead of protected, and trimming down extraneous method arguments helps to future proof the public interfaces. This means that you can expect 4.x to be the major version of Guzzle for the next few years. Simpler interfaces makes it easier for others to extend and implement the interfaces allowing for things like decorators and makes it easier to understand how the library works.

Some examples of this are:

  • Requests no longer have a reference to a client or response. Clients send requests and transaction objects act as a mediator between a client, request, and response. The boundaries and responsibilities of these three collaborators are now more clearly defined.
  • There’s no longer an interface separation between requests that can have a body and requests that can’t have a body.
  • Requests no longer have state. They’re much closer to value objects now.
  • Various methods of Guzzle\Http\Client have been removed in favor of a more flexible manner of configuring default client request options.
  • HTTP message headers are no longer header objects but rather strings and arrays.
  • There’s now no difference between entity bodies and streams. Guzzle 4 now just uses streams.

Smaller Core Library

Various components have been pulled out into their own libraries to help make Guzzle a smaller library focused on sending HTTP requests. Guzzle’s core library previously had iterators, inflectors, a batching abstraction, a service description layer, and many more components. The problem with creating such a monolithic repository is that it becomes extremely difficult to follow semver and to continue to progress the components independently of one another. A breaking change in even the most infrequently used corner of Guzzle 3 would warrant a major version bump or encourage a maintainer to not strictly follow semver.

Guzzle 4 has removed everything form the core Guzzle library that isn’t required to send HTTP requests. The service description layer, most plugins, and various other components have been moved to their own repository. This means that you can expect Guzzle 4 to strictly follow semver.

A More Coherent Event System

The event system in Guzzle 4 is much more coherent and better defined.

  • There are now fewer lifecycle events emitted for a request
  • There are now named event priorities that serve as a landmark for common event priorities. For example GuzzleHttp\Event\RequestEvents::SIGN_REQUEST can be used for all event subscribers that sign HTTP requests.
  • Errors that occur while transferring requests now consistently emit an “error” event. Previous versions of Guzzle had a “request.exception” and “request.error” event which basically served the same purpose, but just made them hard to work with.

Guzzle now ships with its own custom event emitter based on the Symfony2 EventDispatcher. Guzzle’s event emitter is more lightweight than the Symfony2 EventDispatcher and helps to isolate Guzzle from external versioning issues (e.g., Symfony 3).

You can learn more about the event system in the online documentation.

More Straightforward Error Handling

Clients now only throw exceptions that are a subclass of GuzzleHttp\Exception\RequestException so the errors that can occur while sending a request are now clearly defined.

For example, in Guzzle 3 you had to catch Guzzle\Http\Exception\RequestException and Guzzle\Http\CurlException to catch HTTP protocol errors and networking errors (and this was hard to work with).

<?php

use Guzzle\Http\Exception\RequestException;
use Guzzle\Http\Exception\CurlException;

try {
    $client->get('https://github.com/_abc_123_404')->send();
} catch (BadResponseException $e) {
    // Do something useful.
} catch (CurlException $e) {
    // Do something relevant.
}

Having to catch two different exceptions for a single request is an awkward API. Guzzle 4 now only throws exceptions that extend from GuzzleHttp\Exception\RequestException.

<?php

use GuzzleHttp\Exception\RequestException;

try {
    $client->get('https://github.com/_abc_123_404');
} catch (RequestException $e) {
    echo $e->getRequest();
    if ($e->hasResponse()) {
        echo $e->getResponse();
    }
}

You can find out more about error handling in Guzzle in the error handling section of the documentation.

Batching is Replaced by Async and Rolling Queues

Guzzle 3 would previously send requests in batches. When exceptions occurred during a batch transaction, they were aggregated into an ExceptionCollection and thrown after all of the requests in a batch had either failed or completed. These aggregate exceptions were hard to work with and made Guzzle’s parallel request implementation quite muddy.

Requests that are sent in parallel in Guzzle 4 are no longer batched with aggregate error handling. Instead, you now work with parallel requests in an asynchronous manner using events to handle both “complete” and “error” events. This approach makes it easier to work with parallel requests and easier to implement error handling.

Here’s an example of sending requests in parallel and adding events to each request.

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\ErrorEvent;

// Create a client with an optional base_url
$client = new GuzzleHttp\Client(['base_url' => 'http://httpbin.org']);

// We want to send this array of requests
$requests = [
    $client->createRequest('GET', '/get'),
    $client->createRequest('DELETE', '/delete'),
    $client->createRequest('PUT', '/put', ['body' => 'test'])
];

// Note: sendAll accepts an array or Iterator
$client->sendAll($requests, [
      // Call this function when each request completes
    'complete' => function (CompleteEvent $event) {
        echo 'Completed request to ' . $event->getRequest()->getUrl() . "\n";
        echo 'Response: ' . $event->getResponse()->getBody() . "\n\n";
    },
    // Call this function when a request encounters an error
    'error' => function (ErrorEvent $event) {
        echo 'Request failed: ' . $event->getRequest()->getUrl() . "\n"
        echo $event->getException();
    },
    // Maintain a maximum pool size of 25 concurrent requests.
    'parallel' => 25
]);

Better POST File Support

You can now send multipart/form-data requests with POST files that can be strings, Guzzle streams, or files on disk. Previous versions of Guzzle relied on cURL to construct multipart/form-data requests, and cURL has a limitation that it can only send files from disk. This caused developers to have to write temporary files to disk so that they could be send in a POST request. This is now much easier in Guzzle 4:

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Stream\Stream;

$client = new Client();
$client->post('http://httpbin.org/post', [
    'body' => [
        'field'       => 'abc',
        'other_field' => '123',
        'file_name'   => fopen('/path/to/file', 'r'),
        'other_file'  => Stream::factory('file contents')
    ]
]);

No More Exception Markers

Guzzle no longer wraps every exception with pointless “exception markers”. Exception markers were a popular concept in the PHP community several years ago, which is why there were added to Guzzle. Exception markers that wrap standard library exceptions but provide no additional context are pointless and do nothing but bloat libraries. I hope that this trend in the PHP community ends.

As an example: Guzzle previously included a Guzzle\Exception\InvalidArgumentException that wrapped the standard library’s \InvalidArgumentException. This custom Guzzle exception marker provided absolutely no additional value to the exception. Furthermore, you almost never want to explicitly handle exception like \InvalidArgumentException, so the exception marker doesn’t make much sense.

I didn’t remove all custom exceptions. There are still enough so that you can catch relevant exceptions: https://github.com/guzzle/guzzle/tree/master/src/Exception

Guzzle Service Descriptions

Guzzle service descriptions have been removed from the core library and broken into two components:

Guzzle Commands

The Commands library provides a simple interface for creating high level API clients using an event-based abstraction over Guzzle HTTP requests and responses. Events are emitted for preparing commands, processing commands, and handling errors encountered while executing a command.

  • Commands: Key value pair objects representing an action to take on a web service. Commands have a name and a set of parameters.
  • Models: Models are key value pair objects representing the result of an API operation.

By creating a simple interface for abstracting APIs, it is now easier to build request serializers and response parsers that use different service description formats (e.g., Swagger, RAML, etc…) while still being able to create service description independent event subscribers.

Note: Guzzle Commands is still in beta.

Guzzle Services

Guzzle Services is an implementation of the Guzzle Commands abstraction that implements the Guzzle service description format.

Note: Guzzle Commands is still in beta.

JSON Arrays in Service Descriptions

Various APIs include arrays as the top-level value of their JSON responses. Guzzle 3 had a well-known issue with describing top-level arrays in its service description format. This issue has now been resolved in Guzzle 4’s Guzzle Services component.

Easier to Register Custom Locations

Registering custom request and response locations to implement custom serialization or parsing was really difficult in Guzzle 3. It’s now much easier in the Guzzle Services component.

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Command\Guzzle\GuzzleClient;
use GuzzleHttp\Command\Guzzle\Description;

$client = new Client();

$description = new Description([
    'baseUrl' => 'http://httpbin.org/',
    'operations' => [
        'testing' => [
            'httpMethod' => 'GET',
            'uri' => '/get',
            'responseModel' => 'getResponse',
            'parameters' => [
                'foo' => [
                    'type' => 'string',
                    'location' => 'query'
                ]
            ]
        ]
    ],
    'models' => [
        'getResponse' => [
            'type' => 'object',
            'additionalProperties' => [
                'location' => 'json'
            ]
        ]
    ]
]);

$guzzleClient = new GuzzleClient(
    $client, 
    $description,
    [
        // You can optionally add custom request locations
        'request_locations' => [
            'custom_location' => new CustomRequestLocation()
        ],
        // You can optionally add custom response locations
        'response_locations' => [
            'custom_location' => new CustomResponseLocation()
        ],
    ]
);

$result = $guzzleClient->testing(['foo' => 'bar']);
echo $result['args']['foo'];
// bar

Plugin Rewrites

Various plugins from 3.x have been ported over to Guzzle 4. The following event subscribers are included as part of the core library:

  • Cookie: Adds, extracts, and persists cookies between HTTP requests. Cookies are easily configured using the cookies request option.
  • History: Maintains a list of requests and responses sent using a request or client.
  • HttpError: Throws exceptions when a 4xx or 5xx response is received.
  • Mock: Queues mock responses or exceptions and delivers mock responses or exceptions in a fifo order. The mock subscriber now throws exceptions when the queue of mocked responses is empty.
  • Redirect: Implements adapter agnostic HTTP redirects. Now supports automatically adding the Referer header and specifying the maximum number of redirects. Redirects are easily configured using the allow_redirects request option.

Other plugins were moved to their own repositories:

  • The Log Subscriber now uses PSR-3 loggers.
  • The Retry Subscriber replaces the old BackoffPlugin and allows for much easier and flexible retry policies.
  • The OAuth Subscriber has been mostly rewritten to better follow OAuth 1.0 standards (in response and with the help of this PR).
  • The Message Integrity Subscriber replaces the old MessageIntegrityPlugin and allows for much more customization, can validate response bodies all at once, or validate streaming response bodies when the last byte of a stream is read.

Some plugins were removed because they were deprecated:

  • GuzzleHttp\Plugin\Async has been removed. This plugin never actually worked that well and is often causes confusion. I also think the concept in general is a bad idea. Without waiting on an HTTP response, you have no idea if a request succeeded or if the remote server received all of the data correctly. Severing a socket when all of the data has been sent from a client should be implemented using a custom HTTP adapter.
  • GuzzleHttp\Plugin\CurlAuth has been removed. You can just use the “auth” request option.
  • GuzzleHttp\Plugin\ErrorResponse\ErrorResponsePlugin has been removed. This was implemented poorly and forced too many constraints on how error responses were constructed. This type of behavior should now be implemented using the command event system.

Migrating to Guzzle 4

Have a look at the upgrade guide for information on how to upgrade to Guzzle 4. The good news is that Guzzle 4 uses a new Packagist package name and a different namespace.

Guzzle 3 can still be installed with Composer using guzzle/guzzle. Guzzle 4 is installed using guzzlehttp/guzzle. Guzzle 3 will continue to use the Guzzle\\ namespace, while Guzzle 4 now uses the GuzzleHttp namespace. These changes make it possible to use both Guzzle 3 and Guzzle 4 in the same project without conflicts.

I will not publish PEAR packages or phars for Guzzle 4, but I will continue to do so for Guzzle 3.

What About Guzzle 3?

Guzzle 3 is an extremely popular library. It is still going to be maintained, but development has now moved to https://github.com/guzzle/guzzle3. Issues related to Guzzle 3 should be opened on the guzzle3 GitHub repository.

Testing the Release Candidate

I’ve hopefully convinced you that Guzzle 4 is an amazing evolutionary step for the library. Now I need your help. Guzzle 4 will be in release candidate stage for the next 2 weeks (possibly 3 if significant issues are reported). I’d really appreciate it if people could test the library and provide feedback before a stable 4.0 release is tagged. My motivation for the aggressive release is in hopes that Drupal 8 will adopt the new version.

You can get started with Guzzle 4 with Composer and the online documentation.

{
    "require": {
        "guzzlehttp/guzzle": "4.*"
    },
    "minimum-stability": "RC"
}

Enjoy!

Comments