A simple, type-safe, zero dependency port of the javascript fetch WebApi for PHP.

Overview

PHP Fetch

A simple, type-safe, zero dependency port of the javascript fetch WebApi for PHP.

NOTE: This library is in < 1.0.0 version and as per the Semantic Versioning Spec, breaking changes might occur in minor releases before reaching 1.0.0. Specify your constraints carefully.

Installation

composer require mnavarrocarter/php-fetch

Basic Usage

A simple GET request can be done just calling fetch passing the url:



use function MNC\Http\fetch;

$response = fetch('https://mnavarro.dev');

// Emit the response to stdout
while (($chunk = $response->body()->read()) !== null) {
    echo $chunk;
}

Advanced Usage

Like in the browser's fetch implementation, you can pass a map of options as a second argument:



use function MNC\Http\fetch;
use Castor\Io\Eof;

$response = fetch('https://some-domain.example/some-form', [
    'method' => 'POST',
    'headers' => [
        'Content-Type' => 'application/json',
        'User-Agent' => 'PHP Fetch'
    ],
    'body' => json_encode(['data' => 'value'])
]);

// Emit the response to stdout in chunks
while (true) {
    $chunk = '';
    try {
        $response->body()->read(4096, $chunk);
    } catch (Eof $e) {
        break;
    }
    echo $chunk;
}

At the moment, the only options supported are:

  • method (string): Sets the request method
  • body (resource|string): The request body.
  • headers (array): An associative array of header names and values.
  • follow_redirects (bool): Whether to follow redirects or not. Default is true.
  • protocol_version (string): The http protocol version to use. Default is 1.1.
  • max_redirects (int): The number of times you allow redirecting. Default is 20.

Getting response information

You can get all the information you need from the response using the available api.



use function MNC\Http\fetch;

$response = fetch('https://mnavarro.dev');

echo $response->status()->protocolVersion();  // 1.1
echo $response->status()->code();   // 200
echo $response->status()->reasonPhrase(); // OK
echo $response->headers()->has('content-type'); // true
echo $response->headers()->contains('content-type', 'html'); // true
echo $response->headers()->get('content-type'); // text/html;charset=utf-8
$bytes = '';
echo $response->body()->read(4096, $bytes); // Allocates reader data into $bytes
echo $bytes; // Outputs some bytes from the response body

Exception Handling

A call to fetch can throw two exceptions, which are properly documented.

A MNC\Http\SocketError is thrown when a TCP connection cannot be established with the server. Common scenarios where this may happen include:

  • The server is down
  • The domain name could not be resolved to an ip address (dns)
  • The server took too long to produce a response (timeout)
  • The SSL handshake failed (non trusted certificate)

A MNC\Http\ProtocolError occurs when a connection could be established, and a response was produced by the server, but this response was an error according to the HTTP protocol specification (a status code in the 400 or 500 range). This exception contains the MNC\Http\Response object that the server produced.

The distinction between these two kind of errors is really important since you most likely will be reacting in different ways to each one of them.

Body Buffering

When you call the MNC\Http\Response::body() method you get an instance of Castor\Io\Reader, which is a very simple interface inspired in golang's io.Reader. This interface allows you to read a chunk of bytes until you reach EOF in the data source.

Often times, you don't want to read byte per byte, but get the whole contents of the body as a string at once. This library provides the readAll function as a convenience for that:



use function Castor\Io\readAll;
use function MNC\Http\fetch;

$response = fetch('https://mnavarro.dev');

echo readAll($response->body()); // Buffers all the contents in memory and emits them.

Buffering is a very good convenience, but it needs to be used with care, since it could increase your memory usage up to the size of the file your are fetching. Keep in mind that and use the reader when you are fetching big files.

Handling Common Encodings

Some libraries make their response implementations aware of the content type of a body in a very unreliable way.

For example, Symfony's HTTP client response object contains a toArray() method that returns an array if the body of the response is a json.

Apart from being a leaky abstraction, it is not a good one, since it can fail miserably in content types like text/plain. However, there is big gain in user experience when we provide helpers like these in our apis.

This library provides an approach a bit more safe. If the response headers contain the application/json content type, the Castor\Io\Reader object of the body is internally decorated with a MNC\Http\Encoding\Json object. This object implements both the Reader interface. Checking for the former is the safest way of handling json payloads:



use MNC\Http\Encoding\Json;
use function MNC\Http\fetch;

$response = fetch('https://api.github.com/users/mnavarrocarter', [
    'headers' => [
        'User-Agent' => 'PHP Fetch 1.0' // Github api requires user agent
    ]
]);

$body = $response->body();

if ($body instanceof Json) {
    var_dump($body->decode()); // Dumps the json as an array
} else {
    // The response body is not json encoded
}

This makes the code more maintainable and evolvable, as we can support more encodings in the future, like csv or xml without harming the base api and making more assumptions about our content types than we should.

This way of doing things (small interfaces that encourage composability) is another principle that we have taken from golang's idiosyncrasies.

Working with Standard Headers

HTTP is a very generic protocol in terms of structure. An HTTP response really is just metadata in the form of key value pairs (headers) and the contents of that response itself.

However, there is a set of standardized headers across multiple RFC's that is not good to ignore. They are not part of the HTTP protocol specification, but they are so widespread and commonly used that a good implementation of the protocol should acknowledge them.

This library keeps the protocol pure but provides better apis over standard headers by using the MNC\Http\StandardHeaders class.

The MNC\Http\Response::headers() method returns an instance of MNC\Http\Headers. This object is just a bag of string keys and string values. Names when fetching headers should be provided by you, and as per protocol spec they are case-insensitive.

By using the MNC\Http\StandardHeaders class, you can decorate a MNC\Http\Headers object to provide an api over some standardized and useful headers.



use MNC\Http\StandardHeaders;
use function MNC\Http\fetch;

$response = fetch('https://mnavarro.dev');

$stdHeaders = StandardHeaders::from($response);
$lastModified = $stdHeaders->getLastModified()->diff(new DateTimeImmutable(), true)->h;
echo sprintf('This html content was last modified %s hours ago...', $lastModified) . PHP_EOL;

You can use these headers information to handle caching or avoiding reading the whole stream body if is not necessary.

Since these standards headers may not be present in a certain responses, they all can return null.

Function Composition

As a function, fetch can be really verbose if you do not use it with the appropriate patterns. One of these appropriate patterns is composition.

For example, you can compose functions the same way you can compose objects. Wrapping fetch in anonymous functions that define some common default options is, in fact, the recommended way of using fetch, not only in this library but also in the browser one.

For example, the following code defines a function that takes a token as an argument and then returns another function that calls fetch with a simplified api, using the token internally.



use MNC\Http\Encoding\Json;
use function MNC\Http\fetch;

$authenticate = static function (string $token) {
    return static function (string $method, string $path, array $contents = null) use ($token): ?array {
        $url = 'https://my-api-service.example' . $path;
        $response = fetch($url, [
            'method' => $method,
            'headers' => [
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer ' . $token
            ],
            'body' => is_array($contents) ? json_encode($contents) : ''
        ]);

        $body = $response->body();
        if ($body instanceof Json) {
            return $body->decode();
        }
        return null;
    };
};

$client = $authenticate('your-api-token');

$ordersArray = $client('GET', '/orders');
$createdOrderArray = $client('POST', '/orders', ['id' => '1234556']);

Note how the $client function does not expose any details of how fetch works and reduces the interaction with the client classes to PHP primitive types only. Of course, this example lacks exception handling, but the idea is the same.

You can pass that $client variable anywhere in your application and you won't be tying your code to this library, but to a callable with the same signature.

Dependency Injection

Following the previous example, we do not recommend you call fetch directly in your code. At least, not if you are too worried about coupling with a specific HTTP client library that you might replace in the future.

A common pattern I personally use, is that I create an interface for the api client that I need to use.



use MNC\Http\Encoding\Json;
use function MNC\Http\fetch;

// We start with an interface, a well defined contract.
interface ApiClient
{
    public function getOrder(string $id): array;

    public function createOrder(string $id): array;

    public function deleteOrder(string $id): void;
}

// Then, we can have an implementation that uses this library.
final class FetchApiClient implements ApiClient
{
    /**
     * @var callable
     */
    private $client;

    /**
     * @param string $token
     * @return FetchApiClient
     */
    public static function authenticate(string $token): FetchApiClient
    {
        $client = static function (string $method, string $path, array $contents = null) use ($token): ?array {
            $url = 'https://my-api-service.example' . $path;
            $response = fetch($url, [
                'method' => $method,
                'headers' => [
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                    'Authorization' => 'Bearer ' . $token
                ],
                'body' => is_array($contents) ? json_encode($contents) : ''
            ]);

            $body = $response->body();
            if ($body instanceof Json) {
                return $body->decode();
            }
            return null;
        };
        return new self($client);
    }

    /**
     * FetchApiClient constructor.
     * @param callable $client
     */
    public function __construct(callable $client)
    {
        $this->client = $client;
    }

    public function getOrder(string $id): array
    {
        return ($this->client)('GET', '/orders/'.$id);
    }

    public function createOrder(string $id): array
    {
        return ($this->client)('POST', '/orders', [
            'id' => $id
        ]);
    }

    public function deleteOrder(string $id): void
    {
        ($this->client)('DELETE', '/orders/'.$id);
    }
}

You can use the interface in all the services that depend on this connecting to the api service it implements.

Why another HTTP Client?

Maybe you are wondering "Is another HTTP client for PHP necessary"? I think this one is.

Before building it, I did an honest review of the currently available options and enumerated the things that, for me, were lacking in them. I also listed the desired features I would have loved to have by looking at other languages and implementations.

At the end, I came up with a list of 4 principles/reasons for building this client that, considered in combination, are only met in this client.

You don't need PSR-18 HTTP client in your apps

I can only have words of praise for the PHP-FIG and all the standards they have produced for PHP. I'm personally a big fan of all things PSR-7 and I'm always hoping the community sees it's benefits and starts moving to them.

But, when I'm developing my own application and I just need to do a simple HTTP request, I would try to avoid at all costs the verbosity and the bloatness of PSR-18 and their implementations. This is why I made php fetch: for the 90% of simple use cases. If you need an HTTP client to do web scraping, don't use this (you need redirect following, multiplexing, cookie support, plugins for bypassing csrf, javascript engine embedded, etc). But if you need a simple http client to make some requests to an api, you'll find using this library more than enough.

"But what about interoperability and vendor lock-in?" Well, truth is that if you are a responsible programmer, you should be building the code that makes requests to a http endpoint behind a proper abstraction, like an interface. Think something like: ApiService with these possible implementations: GuzzleApiService, CurlApiService or FetchApiService. If you do this, there is no vendor lock-in to be afraid of. On the contrary, if you don't keep your dependencies hidden behind interfaces that serve your own contract and requirements, you will suffer not only when doing HTTP, but with pretty much anything else.

PSR-18 was made for libraries mainly, to avoid dependency conflicts. This does not mean that it cannot be used in applications; many people do, and it works! What it does mean is that its reason to be is to serve libraries, like HTTP SDKS or others. If you are familiar with the whole Guzzle fiasco from a few years ago, you'll know that HTTPPlug (the inspiration for PSR-18) was made with the sole purpose of becoming protection from dependency conflicts in some libraries, mainly caused by a very aggressive release policy from Guzzle, and a very relaxed release policy from Amazon.

So, the simplicity of this library is more than enough for most of my applications.

Most HTTP clients are too bloated

Again, this is not a defect of HTTP clients per se. A client that has many features will have a lot of code and dependencies. The question is whether you need those features for your use case or not. In my experience, most of the time I don't need them, and I always end up doing simple HTTP requests with PHP streams. I built this library so I don't have to do that anymore for simple use cases.

Again, if your use case is more complex, you might want to consider using a more feature rich HTTP client. Symfony Panther is my go-to recommendation for web scraping, for example.

No HTTP client is just a function

One thing I love about working with javascript is its more functional friendly approach. Even though is light-years away of being a pure functional language, the declarative nature of most of it's apis make it really nice to work with (oh if only had proper typing and encapsulation!).

The fetch api is one of my favourites, and I always wanted to have something like that in PHP. I searched for it, but to no avail, hence this library. Sometimes, most of our single method classes could perfectly be functions.

Some people in PHP are starting to grasp this and using more functions, especially since functions can be namespaced now (please never add functions in the global namespace!).

Immutability

Well, PSR-18 favours immutability in the form of reference clonation. This library does it in the form of read-only state. There is nothing you can change in an already constructed response. Everything is read only.

Well, you could change it using nasty php tricks like closure scope binding; but don't do that, okay?

There is no reason why you should need to change a response from a server. The only thing you can do with the response is compose it into other types: nothing more.

Other Nit-Pickyness

It really annoys the freak out of me when a library that implements a protocol does not throw exceptions when a protocol error happens. Like, which SMTP client library gives you a Response containing an error when no target address has been specified instead of throwing an exception? How are you supposed to know that an error happened?

The purpose of implementing a protocol in a language is to mimic the idiosyncrasies of the protocol in the available language constructs. If there is status codes defined for errors, the language construct for errors should be used: in this case, an exception should be raised.

This is one of the biggest problems for me with PSR-18. I think it was a terrible design decision that harms user experience.

In Closing

This client is simple, small, functional, immutable, type-safe, well designed and achieves a good balance between protocol strictness and convenience. I think there is nothing like that in the PHP ecosystem right now, so there might be a user base for this.

Hope you enjoy using it as much as I enjoyed building it.

Comments
  • Missing dep: symfony/process

    Missing dep: symfony/process

    If I go with only

            "phpunit/phpunit": "^9.4",
            "symfony/var-dumper": "^5.1",
            "amphp/http-server": "^2.1",
            "amphp/http-server-static-content": "^1.0",
            "mnavarrocarter/amp-http-router": "^0.1.0",
    

    symfony/process will be missing.

    opened by szepeviktor 4
  • What feof() does

    What feof() does

            if (feof($this->resource)) {
                return null;
            }
            $result = @fread($this->resource, $bytes);
    

    What does feof on HTTP level? How can it tell that the stream is over? As I understand fread fires a new HTTP request each time. Am I wrong?

    opened by szepeviktor 3
  • Fix Composer usage in CI

    Fix Composer usage in CI

    You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3. No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.

    opened by szepeviktor 3
  • Imprvoe PHP version requirements

    Imprvoe PHP version requirements

    Changed log

    • Adding the php-8.0 version for requiring this PHP package.
    • Removing the --ignore-platform-reqs option because it doesn't need to ignore platform requirements when running composer install command.
    opened by peter279k 0
  • Specify an Exception Marker Interface for all exception thrown by this library

    Specify an Exception Marker Interface for all exception thrown by this library

    <?php
    
    interface ClientError implements Throwable
    {
    }
    
    class SocketErorr extends Exception implements ClientError
    {
    }
    

    So that any consuming application can easily isolate error and exception thrown by the library by typeHinting for ClientError

    enhancement 
    opened by nyamsprod 0
  • Put errors in conditionals

    Put errors in conditionals

    and do not indent normal execution.

                if (strpos($line, 'HTTP') === 0) {
                    $status = Status::fromStatusLine($line);
                    $headers = Headers::fromLines($lines);
                    $partials[] = new HttpPartialResponse($status, $headers);
                }
    
    			// 👇 
    
                if (strpos($line, 'HTTP') !== 0) {
    				continue;
                }
    
                $status = Status::fromStatusLine($line);
                $headers = Headers::fromLines($lines);
                $partials[] = new HttpPartialResponse($status, $headers);
    

    How about that?

    enhancement 
    opened by szepeviktor 0
  • Use temp. variables?

    Use temp. variables?

    After Adam Wathan: https://youtu.be/crSUWtRYw-M?t=615 (item number #3)

    
                    $status = Status::fromStatusLine($line);
                    $headers = Headers::fromLines($lines);
                    $partials[] = new HttpPartialResponse($status, $headers);
    
                    // 👇 
    
                    $partials[] = new HttpPartialResponse(Status::fromStatusLine($line), Headers::fromLines($lines));
    

    What do you think?

    enhancement 
    opened by szepeviktor 0
Releases(0.3.0)
  • 0.3.0(Apr 29, 2021)

  • 0.2.0(Nov 12, 2020)

    This is a backward incompatible release:

    Changes

    • The MNC\Http\Enconding\JsonReader interface has been removed. It did not do anything that the normal reading plus buffering could not do.
    • The MNC\Http\Response::reasonPhrase method has been removed. Not it is implemented in MNC\Http\Status::reasonPhrase
    • MNC\Http\Response is now an interface.

    New Features

    • Responses can implement MNC\Http\Redirected interface when a one or more redirects have been followed. A collection of MNC\Http\PartialResponse can be obtained from them with the headers and status of the previous redirects.
    Source code(tar.gz)
    Source code(zip)
  • 0.1.0(Nov 11, 2020)

Owner
Matias Navarro Carter
406d6e61766172726f636172746572
Matias Navarro Carter
A collection of type-safe functional data structures

lamphpda A collection of type-safe functional data structures Aim The aim of this library is to provide a collection of functional data structures in

Marco Perone 99 Nov 11, 2022
A PHP/Laravel package to fetch Notion Pages and convert it to HTML!

Generate HTML from Notion Page This package converts all the blocks in a Notion page into HTML using Notion's API. For more details on Notion API, ple

Usama Rehan 4 Nov 23, 2022
a Telegram bot to fetch download link from pan.baidu.com

baiduwp-bot a Telegram bot to fetch download link from pan.baidu.com What it can do Get a file download link from share link. 从分享链接获取下载地址 How to use G

Yuan_Tuo 23 Dec 3, 2022
The Current US Version of PHP-Nuke Evolution Xtreme v3.0.1b-beta often known as Nuke-Evolution Xtreme. This is a hardened version of PHP-Nuke and is secure and safe. We are currently porting Xtreme over to PHP 8.0.3

2021 Nightly Builds Repository PHP-Nuke Evolution Xtreme Developers TheGhost - Ernest Allen Buffington (Lead Developer) SeaBeast08 - Sebastian Scott B

Ernest Buffington 7 Aug 28, 2022
Simple, single-file and dependency-free AWS S3 client.

Simple, single-file and dependency-free AWS S3 client. Why? In some scenarios we want the simplest and lightest S3 client possible. For example in Bre

Matthieu Napoli 28 Nov 15, 2022
Easy to use utility functions for everyday PHP projects. This is a port of the Lodash JS library to PHP

Lodash-PHP Lodash-PHP is a port of the Lodash JS library to PHP. It is a set of easy to use utility functions for everyday PHP projects. Lodash-PHP tr

Lodash PHP 474 Dec 31, 2022
Make your PHP arrays sweet'n'safe

Mess We face a few problems in our PHP projects Illogical type casting (PHP's native implementation is way too "smart") Pointless casts like array =>

Artem Zakirullin 192 Nov 28, 2022
This is the US hardened version of PHP-Nuke Titanium and is secure and safe

This is the US hardened version of PHP-Nuke Titanium and is secure and safe. Built on PHP Version 7.4.30 - Forums - Blogs - Projects - Advanced Site Map - Web Links - Groups - Advanced Theme Support - Downloads - Advertising - Network Advertising - Link Back System - FAQ - Bookmark Vault - Private Virtual Cemetery - Loan Amortization - Image Hosting

Ernest Allen Buffington (The Ghost) 12 Dec 25, 2022
A quick,easy and safe way of accessing Mysql-like databases from within a PHP program

Mysqli-Safe A simple, easy-to-use and secure way of accessing a Mysql database from within your PHP programs Mysqli-safe is a wrapper around the mysql

Anthony Maina Njoroge 2 Oct 9, 2022
A redacted PHP port of Underscore.js with additional functions and goodies – Available for Composer and Laravel

Underscore.php The PHP manipulation toolbelt First off : Underscore.php is not a PHP port of Underscore.js (well ok I mean it was at first). It's does

Emma Fabre 1.1k Dec 11, 2022
Port of the Java Content Repository (JCR) to PHP.

PHP Content Repository PHPCR This repository contains interfaces for the PHPCR standard. The JSR-283 specification defines an API for a Content Reposi

PHPCR 436 Dec 30, 2022
Back the fun of reading - PHP Port for Arc90′s Readability

PHP Readability Library If you want to use an up-to-date version of this algorithm,check this newer project: https://github.com/andreskrey/readability

明城 517 Nov 18, 2022
An improved version of the PHP port of KuzuhaScript

KuzuhaScriptPHP+ (くずはすくりぷとPHP+) An improved version of the PHP port of KuzuhaScript (くずはすくりぷと). To my knowledge, it works with PHP version 4.1.0 and a

Heyuri 4 Nov 16, 2022
A PHP port of Ruby's Liquid Templates

Liquid template engine for PHP Liquid is a PHP port of the Liquid template engine for Ruby, which was written by Tobias Lutke. Although there are many

Alexander Guz 141 Nov 4, 2022
JsonQ is a simple, elegant PHP package to Query over any type of JSON Data

php-jsonq JsonQ is a simple, elegant PHP package to Query over any type of JSON Data. It'll make your life easier by giving the flavour of an ORM-like

Nahid Bin Azhar 834 Dec 25, 2022
BetterWPDB - Keeps you safe and sane when working with custom tables in WordPress.

BetterWPDB - Keeps you safe and sane when working with custom tables in WordPress.

Snicco 21 Dec 15, 2022
This is a port of the original WireGuard UI bits as implemented by Netgate in pfSense 2.5.0 to a package suitable for rapid iteration and more frequent updating on future releases of pfSense.

This is a port of the original WireGuard*** UI bits as implemented by Netgate in pfSense 2.5.0 to a package suitable for sideloading and more frequent updating on future releases of pfSense. This also includes some improvments such as a proper status page (found under Status / WireGuard Status) and improved assigned interface handling.

R. Christian McDonald 195 Dec 23, 2022
A class for easy connection to the zarinpal port

Payment class with ZarinPal A class to simplify payment operations and confirm payment of ZarrinPal payment gateway service ( به فارسی بخوانید ) Insta

Mohammad Qasemi 7 Jul 15, 2022