Chris@0: # HTTP Clients and zend-feed Chris@0: Chris@0: Several operations in zend-feed's Reader subcomponent require an HTTP client: Chris@0: Chris@0: - importing a feed Chris@0: - finding links in a feed Chris@0: Chris@0: In order to allow developers a choice in HTTP clients, the subcomponent defines Chris@0: several interfaces and classes. Elsewhere in the documentation, we reference Chris@0: where an HTTP client may be used; this document details what constitutes an HTTP Chris@0: client and its behavior, and some of the concrete classes available within the Chris@0: component for implementing this behavior. Chris@0: Chris@0: ## ClientInterface and HeaderAwareClientInterface Chris@0: Chris@0: First, we define two interfaces for clients, Chris@0: `Zend\Feed\Reader\Http\ClientInterface` and `HeaderAwareClientInterface`: Chris@0: Chris@0: ```php Chris@0: namespace Zend\Feed\Reader\Http; Chris@0: Chris@0: interface ClientInterface Chris@0: { Chris@0: /** Chris@0: * Make a GET request to a given URL. Chris@0: * Chris@0: * @param string $url Chris@0: * @return ResponseInterface Chris@0: */ Chris@0: public function get($url); Chris@0: } Chris@0: Chris@0: interface HeaderAwareClientInterface extends ClientInterface Chris@0: { Chris@0: /** Chris@0: * Make a GET request to a given URL. Chris@0: * Chris@0: * @param string $url Chris@0: * @param array $headers Chris@0: * @return ResponseInterface Chris@0: */ Chris@0: public function get($url, array $headers = []); Chris@0: } Chris@0: ``` Chris@0: Chris@0: The first is header-agnostic, and assumes that the client will simply perform an Chris@0: HTTP GET request. The second allows providing headers to the client; typically, Chris@0: these are used for HTTP caching headers. `$headers` must be in the following Chris@0: structure: Chris@0: Chris@0: ```php Chris@0: $headers = [ Chris@0: 'X-Header-Name' => [ Chris@0: 'header', Chris@0: 'values', Chris@0: ], Chris@0: ]; Chris@0: ``` Chris@0: Chris@0: i.e., each key is a header name, and each value is an array of values for that Chris@0: header. If the header represents only a single value, it should be an array with Chris@0: that value: Chris@0: Chris@0: ```php Chris@0: $headers = [ Chris@0: 'Accept' => [ 'application/rss+xml' ], Chris@0: ]; Chris@0: ``` Chris@0: Chris@0: A call to `get()` should yield a *response*. Chris@0: Chris@0: ## ResponseInterface and HeaderAwareResponseInterface Chris@0: Chris@0: Responses are modeled using `Zend\Feed\Reader\Http\ResponseInterface` and Chris@0: `HeaderAwareResponseInterface`: Chris@0: Chris@0: ```php Chris@0: namespace Zend\Feed\Reader\Http; Chris@0: Chris@0: class ResponseInterface Chris@0: { Chris@0: /** Chris@0: * Retrieve the status code. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getStatusCode(); Chris@0: Chris@0: /** Chris@0: * Retrieve the response body contents. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getBody(); Chris@0: } Chris@0: Chris@0: class HeaderAwareResponseInterface extends ResponseInterface Chris@0: { Chris@0: /** Chris@0: * Retrieve a named header line. Chris@0: * Chris@0: * Retrieve a header by name; all values MUST be concatenated to a single Chris@0: * line. If no matching header is found, return the $default value. Chris@0: * Chris@0: * @param string $name Chris@0: * @param null|string $default Chris@0: * @return string Chris@0: public function getHeaderLine($name, $default = null); Chris@0: } Chris@0: ``` Chris@0: Chris@0: Internally, `Reader` will typehint against `ClientInterface` for the bulk of Chris@0: operations. In some cases, however, certain capabilities are only possible if Chris@0: the response can provide headers (e.g., for caching); in such cases, it will Chris@0: check the instance against `HeaderAwareResponseInterface`, and only call Chris@0: `getHeaderLine()` if it matches. Chris@0: Chris@0: ## Response Chris@0: Chris@0: zend-feed ships with a generic `ResponseInterface` implementation, Chris@0: `Zend\Feed\Http\Response`. It implements `HeaderAwareResponseInterface`, and Chris@0: defines the following constructor: Chris@0: Chris@0: ```php Chris@0: namespace Zend\Feed\Reader\Http; Chris@0: Chris@0: class Response implements HeaderAwareResponseInterface Chris@0: { Chris@0: /** Chris@0: * Constructor Chris@0: * Chris@0: * @param int $statusCode Response status code Chris@0: * @param string $body Response body Chris@0: * @param array $headers Response headers, if available Chris@0: */ Chris@0: public function __construct($statusCode, $body, array $headers = []); Chris@0: } Chris@0: ``` Chris@0: Chris@0: ## PSR-7 Response Chris@0: Chris@0: [PSR-7](http://www.php-fig.org/psr/psr-7/) defines a set of HTTP message Chris@0: interfaces, but not a client interface. To facilitate wrapping an HTTP client Chris@0: that uses PSR-7 messages, we provide `Zend\Feed\Reader\Psr7ResponseDecorator`: Chris@0: Chris@0: ```php Chris@0: namespace Zend\Feed\Reader\Http; Chris@0: Chris@0: use Psr\Http\Message\ResponseInterface as PsrResponseInterface; Chris@0: Chris@0: class Psr7ResponseDecorator implements HeaderAwareResponseInterface Chris@0: { Chris@0: /** Chris@0: * @param PsrResponseInterface $response Chris@0: */ Chris@0: public function __construct(PsrResponseInterface $response); Chris@0: Chris@0: /** Chris@0: * @return PsrResponseInterface Chris@0: */ Chris@0: public function getDecoratedResponse(); Chris@0: } Chris@0: ``` Chris@0: Chris@0: Clients can then take the PSR-7 response they receive, pass it to the decorator, Chris@0: and return the decorator. Chris@0: Chris@0: To use the PSR-7 response, you will need to add the PSR-7 interfaces to your Chris@0: application, if they are not already installed by the client of your choice: Chris@0: Chris@0: ```bash Chris@0: $ composer require psr/http-message Chris@0: ``` Chris@0: Chris@0: ## zend-http Chris@0: Chris@0: We also provide a zend-http client decorator, Chris@0: `Zend\Feed\Reader\Http\ZendHttpClientDecorator`: Chris@0: Chris@0: ```php Chris@0: namespace Zend\Feed\Reader\Http; Chris@0: Chris@0: use Zend\Http\Client as HttpClient; Chris@0: Chris@0: class ZendHttpClientDecorator implements HeaderAwareClientInterface Chris@0: { Chris@0: /** Chris@0: * @param HttpClient $client Chris@0: */ Chris@0: public function __construct(HttpClient $client); Chris@0: Chris@0: /** Chris@0: * @return HttpClient Chris@0: */ Chris@0: public function getDecoratedClient(); Chris@0: } Chris@0: ``` Chris@0: Chris@0: Its `get()` implementation returns a `Response` instance seeded from the Chris@0: zend-http response returned, including status, body, and headers. Chris@0: Chris@0: zend-http is the default implementation assumed by `Zend\Feed\Reader\Reader`, Chris@0: but *is not installed by default*. You may install it using composer: Chris@0: Chris@0: ```bash Chris@0: $ composer require zendframework/zend-http Chris@0: ``` Chris@0: Chris@0: ## Providing a client to Reader Chris@0: Chris@0: By default, `Zend\Feed\Reader\Reader` will lazy load a zend-http client. If you Chris@0: have not installed zend-http, however, PHP will raise an error indicating the Chris@0: class is not found! Chris@0: Chris@0: As such, you have two options: Chris@0: Chris@0: 1. Install zend-http: `composer require zendframework/zend-http`. Chris@0: 2. Inject the `Reader` with your own HTTP client. Chris@0: Chris@0: To accomplish the second, you will need an implementation of Chris@0: `Zend\Feed\Reader\Http\ClientInterface` or `HeaderAwareClientInterface`, and an Chris@0: instance of that implementation. Once you do, you can use the static method Chris@0: `setHttpClient()` to inject it. Chris@0: Chris@0: As an example, let's say you've created a PSR-7-based implementation named Chris@0: `My\Http\Psr7FeedClient`. You could then do the following: Chris@0: Chris@0: ```php Chris@0: use My\Http\Psr7FeedClient; Chris@0: use Zend\Feed\Reader\Reader; Chris@0: Chris@0: Reader::setHttpClient(new Psr7FeedClient()); Chris@0: ``` Chris@0: Chris@0: Your client will then be used for all `import()` and `findFeedLinks()` Chris@0: operations.