{"id":15578490,"url":"https://github.com/mnavarrocarter/php-fetch","last_synced_at":"2025-10-06T23:28:03.792Z","repository":{"id":52428476,"uuid":"311767815","full_name":"mnavarrocarter/php-fetch","owner":"mnavarrocarter","description":"A simple, type-safe, zero dependency port of the javascript fetch WebApi for PHP","archived":false,"fork":false,"pushed_at":"2021-04-29T15:18:24.000Z","size":68,"stargazers_count":102,"open_issues_count":0,"forks_count":3,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-08-18T00:38:02.735Z","etag":null,"topics":["fetch","fetch-api","http","http-client","php","php7","response","streams","web-api"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mnavarrocarter.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-11-10T19:48:01.000Z","updated_at":"2025-08-06T12:59:26.000Z","dependencies_parsed_at":"2022-08-22T11:31:22.186Z","dependency_job_id":null,"html_url":"https://github.com/mnavarrocarter/php-fetch","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/mnavarrocarter/php-fetch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnavarrocarter%2Fphp-fetch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnavarrocarter%2Fphp-fetch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnavarrocarter%2Fphp-fetch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnavarrocarter%2Fphp-fetch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mnavarrocarter","download_url":"https://codeload.github.com/mnavarrocarter/php-fetch/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnavarrocarter%2Fphp-fetch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271579431,"owners_count":24784250,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-22T02:00:08.480Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fetch","fetch-api","http","http-client","php","php7","response","streams","web-api"],"created_at":"2024-10-02T19:10:52.274Z","updated_at":"2025-10-06T23:27:58.744Z","avatar_url":"https://github.com/mnavarrocarter.png","language":"PHP","readme":"\u003ch1 align=\"center\"\u003ePHP Fetch\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003eA simple, type-safe, zero dependency port of the javascript fetch WebApi for PHP.\u003c/p\u003e\n\n\u003ch3 align=\"center\"\u003e\n    \u003cimg style=\"alignment: center\" src=\"https://media0.giphy.com/media/xlYKItjhiDsY/giphy.gif?cid=ecf05e474io66b5jt2mrufubg3otjzq26qgtqd0cb0w71fiu\u0026rid=giphy.gif\"/\u003e\n\u003c/h3\u003e\n\n\u003e NOTE: This library is in `\u003c 1.0.0` version and as per the Semantic Versioning Spec, breaking\n\u003e changes might occur in minor releases before reaching `1.0.0`. Specify your constraints \n\u003e carefully.\n\n## Installation\n\n```bash\ncomposer require mnavarrocarter/php-fetch\n```\n\n## Basic Usage\n\nA simple `GET` request can be done just calling fetch passing the url:\n\n```php\n\u003c?php\n\nuse function MNC\\Http\\fetch;\n\n$response = fetch('https://mnavarro.dev');\n\n// Emit the response to stdout\nwhile (($chunk = $response-\u003ebody()-\u003eread()) !== null) {\n    echo $chunk;\n}\n```\n\n## Advanced Usage\n\nLike in the browser's `fetch` implementation, you can pass a map of options\nas a second argument:\n\n```php\n\u003c?php\n\nuse function MNC\\Http\\fetch;\nuse Castor\\Io\\Eof;\n\n$response = fetch('https://some-domain.example/some-form', [\n    'method' =\u003e 'POST',\n    'headers' =\u003e [\n        'Content-Type' =\u003e 'application/json',\n        'User-Agent' =\u003e 'PHP Fetch'\n    ],\n    'body' =\u003e json_encode(['data' =\u003e 'value'])\n]);\n\n// Emit the response to stdout in chunks\nwhile (true) {\n    $chunk = '';\n    try {\n        $response-\u003ebody()-\u003eread(4096, $chunk);\n    } catch (Eof $e) {\n        break;\n    }\n    echo $chunk;\n}\n```\n\nAt the moment, the only options supported are:\n\n- `method (string)`: Sets the request method\n- `body (resource|string)`: The request body.\n- `headers (array\u003cstring|string\u003e)`: An associative array of header names and values.\n- `follow_redirects (bool)`: Whether to follow redirects or not. Default is `true`.\n- `protocol_version (string)`: The http protocol version to use. Default is `1.1`.\n- `max_redirects (int)`: The number of times you allow redirecting. Default is `20`.\n\n### Getting response information\n\nYou can get all the information you need from the response using\nthe available api.\n\n```php\n\u003c?php\n\nuse function MNC\\Http\\fetch;\n\n$response = fetch('https://mnavarro.dev');\n\necho $response-\u003estatus()-\u003eprotocolVersion();  // 1.1\necho $response-\u003estatus()-\u003ecode();   // 200\necho $response-\u003estatus()-\u003ereasonPhrase(); // OK\necho $response-\u003eheaders()-\u003ehas('content-type'); // true\necho $response-\u003eheaders()-\u003econtains('content-type', 'html'); // true\necho $response-\u003eheaders()-\u003eget('content-type'); // text/html;charset=utf-8\n$bytes = '';\necho $response-\u003ebody()-\u003eread(4096, $bytes); // Allocates reader data into $bytes\necho $bytes; // Outputs some bytes from the response body\n```\n\n### Exception Handling\n\nA call to `fetch` can throw two exceptions, which are properly documented.\n\nA `MNC\\Http\\SocketError` is thrown when a TCP connection cannot be established\nwith the server. Common scenarios where this may happen include:\n\n- The server is down\n- The domain name could not be resolved to an ip address (dns)\n- The server took too long to produce a response (timeout)\n- The SSL handshake failed (non trusted certificate)\n\nA `MNC\\Http\\ProtocolError` occurs when a connection could be established, and a\nresponse was produced by the server, but this response was an error according to\nthe HTTP protocol specification (a status code in the 400 or 500 range). This exception\ncontains the `MNC\\Http\\Response` object that the server produced.\n\nThe distinction between these two kind of errors is really important since\nyou most likely will be reacting in different ways to each one of them.\n\n### Body Buffering\n\nWhen you call the `MNC\\Http\\Response::body()` method you get an instance of\n`Castor\\Io\\Reader`, which is a very simple interface inspired in golang's\n`io.Reader`. This interface allows you to read a chunk of bytes until you reach\n`EOF` in the data source.\n\nOften times, you don't want to read byte per byte, but get the whole contents \nof the body as a string at once. This library provides the `readAll` function\nas a convenience for that:\n\n```php\n\u003c?php\n\nuse function Castor\\Io\\readAll;\nuse function MNC\\Http\\fetch;\n\n$response = fetch('https://mnavarro.dev');\n\necho readAll($response-\u003ebody()); // Buffers all the contents in memory and emits them.\n````\n\nBuffering is a very good convenience, but it needs to be used with care, since it could\nincrease your memory usage up to the size of the file your are fetching. Keep in mind that\nand use the reader when you are fetching big files.\n\n### Handling Common Encodings\n\nSome libraries make their response implementations aware of the content type of a body in a\nvery unreliable way.\n\nFor example, Symfony's HTTP client response object contains a `toArray()` method that\nreturns an array if the body of the response is a json.\n\nApart from being a leaky abstraction, it is not a good one, since it can fail miserably\nin content types like `text/plain`. However, there is big gain in user experience\nwhen we provide helpers like these in our apis.\n\nThis library provides an approach a bit more safe. If the response headers contain the\n`application/json` content type, the ``Castor\\Io\\Reader`` object of the body is internally\ndecorated with a `MNC\\Http\\Encoding\\Json` object. This object implements both the\n`Reader` interface. Checking for the former is the safest way of handling json\npayloads:\n\n```php\n\u003c?php\n\nuse MNC\\Http\\Encoding\\Json;\nuse function MNC\\Http\\fetch;\n\n$response = fetch('https://api.github.com/users/mnavarrocarter', [\n    'headers' =\u003e [\n        'User-Agent' =\u003e 'PHP Fetch 1.0' // Github api requires user agent\n    ]\n]);\n\n$body = $response-\u003ebody();\n\nif ($body instanceof Json) {\n    var_dump($body-\u003edecode()); // Dumps the json as an array\n} else {\n    // The response body is not json encoded\n}\n```\n\nThis makes the code more maintainable and evolvable, as we can support more encodings \nin the future, like `csv` or `xml` without harming the base api and making more assumptions\nabout our content types than we should.\n\nThis way of doing things (small interfaces that encourage composability) is another principle\nthat we have taken from golang's idiosyncrasies.\n\n### Working with Standard Headers\n\nHTTP is a very generic protocol in terms of structure. An HTTP response really is just\nmetadata in the form of key value pairs (headers) and the contents of that response itself.\n\nHowever, there is a set of standardized headers across multiple RFC's that is not good to\nignore. They are not part of the HTTP protocol specification, but they are so widespread\nand commonly used that a good implementation of the protocol should acknowledge them.\n\nThis library keeps the protocol pure but provides better apis over standard headers by\nusing the `MNC\\Http\\StandardHeaders` class.\n\nThe `MNC\\Http\\Response::headers()` method returns an instance of `MNC\\Http\\Headers`.\nThis object is just a bag of string keys and string values. Names when fetching headers\nshould be provided by you, and as per protocol spec they are case-insensitive.\n\nBy using the `MNC\\Http\\StandardHeaders` class, you can decorate a `MNC\\Http\\Headers`\nobject to provide an api over some standardized and useful headers.\n\n```php\n\u003c?php\n\nuse MNC\\Http\\StandardHeaders;\nuse function MNC\\Http\\fetch;\n\n$response = fetch('https://mnavarro.dev');\n\n$stdHeaders = StandardHeaders::from($response);\n$lastModified = $stdHeaders-\u003egetLastModified()-\u003ediff(new DateTimeImmutable(), true)-\u003eh;\necho sprintf('This html content was last modified %s hours ago...', $lastModified) . PHP_EOL;\n```\n\nYou can use these headers information to handle caching or avoiding reading the whole\nstream body if is not necessary.\n\nSince these standards headers may not be present in a certain responses, they\nall can return `null`.\n \n### Function Composition\n\nAs a function, `fetch` can be really verbose if you do not use it with the\nappropriate patterns. One of these appropriate patterns is composition.\n\nFor example, you can compose functions the same way you can\ncompose objects. Wrapping `fetch` in anonymous functions that define\nsome common default options is, in fact, the recommended way of using `fetch`, not\nonly in this library but also in the browser one.\n\nFor example, the following code defines a function that takes a token as an\nargument and then returns another function that calls fetch with a simplified\napi, using the token internally.\n\n```php\n\u003c?php\n\nuse MNC\\Http\\Encoding\\Json;\nuse function MNC\\Http\\fetch;\n\n$authenticate = static function (string $token) {\n    return static function (string $method, string $path, array $contents = null) use ($token): ?array {\n        $url = 'https://my-api-service.example' . $path;\n        $response = fetch($url, [\n            'method' =\u003e $method,\n            'headers' =\u003e [\n                'Accept' =\u003e 'application/json',\n                'Content-Type' =\u003e 'application/json',\n                'Authorization' =\u003e 'Bearer ' . $token\n            ],\n            'body' =\u003e is_array($contents) ? json_encode($contents) : ''\n        ]);\n\n        $body = $response-\u003ebody();\n        if ($body instanceof Json) {\n            return $body-\u003edecode();\n        }\n        return null;\n    };\n};\n\n$client = $authenticate('your-api-token');\n\n$ordersArray = $client('GET', '/orders');\n$createdOrderArray = $client('POST', '/orders', ['id' =\u003e '1234556']);\n```\n\nNote how the `$client` function does not expose any details of how fetch works\nand reduces the interaction with the client classes to PHP primitive types\nonly. Of course, this example lacks exception handling, but the idea is the same.\n\nYou can pass that `$client` variable anywhere in your application and you won't\nbe tying your code to this library, but to a callable with the same signature.\n\n### Dependency Injection\n\nFollowing the previous example, we do not recommend you call fetch directly in\nyour code. At least, not if you are too worried about coupling with a specific\nHTTP client library that you might replace in the future.\n\nA common pattern I personally use, is that I create an interface for the\napi client that I need to use.\n\n```php\n\u003c?php\n\nuse MNC\\Http\\Encoding\\Json;\nuse function MNC\\Http\\fetch;\n\n// We start with an interface, a well defined contract.\ninterface ApiClient\n{\n    public function getOrder(string $id): array;\n\n    public function createOrder(string $id): array;\n\n    public function deleteOrder(string $id): void;\n}\n\n// Then, we can have an implementation that uses this library.\nfinal class FetchApiClient implements ApiClient\n{\n    /**\n     * @var callable\n     */\n    private $client;\n\n    /**\n     * @param string $token\n     * @return FetchApiClient\n     */\n    public static function authenticate(string $token): FetchApiClient\n    {\n        $client = static function (string $method, string $path, array $contents = null) use ($token): ?array {\n            $url = 'https://my-api-service.example' . $path;\n            $response = fetch($url, [\n                'method' =\u003e $method,\n                'headers' =\u003e [\n                    'Accept' =\u003e 'application/json',\n                    'Content-Type' =\u003e 'application/json',\n                    'Authorization' =\u003e 'Bearer ' . $token\n                ],\n                'body' =\u003e is_array($contents) ? json_encode($contents) : ''\n            ]);\n\n            $body = $response-\u003ebody();\n            if ($body instanceof Json) {\n                return $body-\u003edecode();\n            }\n            return null;\n        };\n        return new self($client);\n    }\n\n    /**\n     * FetchApiClient constructor.\n     * @param callable $client\n     */\n    public function __construct(callable $client)\n    {\n        $this-\u003eclient = $client;\n    }\n\n    public function getOrder(string $id): array\n    {\n        return ($this-\u003eclient)('GET', '/orders/'.$id);\n    }\n\n    public function createOrder(string $id): array\n    {\n        return ($this-\u003eclient)('POST', '/orders', [\n            'id' =\u003e $id\n        ]);\n    }\n\n    public function deleteOrder(string $id): void\n    {\n        ($this-\u003eclient)('DELETE', '/orders/'.$id);\n    }\n}\n```\n\nYou can use the interface in all the services that depend on this connecting\nto the api service it implements.\n\n## Why another HTTP Client?\n\nMaybe you are wondering \"Is another HTTP client for PHP necessary\"? I think this one is.\n\nBefore building it, I did an honest review of the currently available options and enumerated\nthe things that, for me, were lacking in them. I also listed the desired features I would have\nloved to have by looking at other languages and implementations.\n\nAt the end, I came up with a list of 4 principles/reasons for building this client that,\n**considered in combination**, are only met in this client.\n\n### You don't need PSR-18 HTTP client in your apps\n\nI can only have words of praise for the PHP-FIG and all the standards they have produced\nfor PHP. I'm personally a big fan of all things PSR-7 and I'm always hoping the community\nsees it's benefits and starts moving to them. \n\nBut, when I'm developing my own application and I just need to do a simple HTTP request, \nI would try to avoid at all costs the verbosity and the bloatness of PSR-18 and their\nimplementations. This is why I made php fetch: for the 90% of simple use cases. If you\nneed an HTTP client to do web scraping, don't use this (you need redirect following,\nmultiplexing, cookie support, plugins for bypassing csrf, javascript engine embedded, etc).\nBut if you need a simple http client to make some requests to an api, you'll find\nusing this library more than enough.\n\n\"But what about interoperability and vendor lock-in?\" Well, truth is that if you are a\nresponsible programmer, you should be building the code that makes requests to a http\nendpoint behind a proper abstraction, like an interface. Think something like: `ApiService`\nwith these possible implementations: `GuzzleApiService`, `CurlApiService` or `FetchApiService`.\nIf you do this, there is no vendor lock-in to be afraid of. On the contrary, if you don't keep\nyour dependencies hidden behind interfaces that serve your own contract and requirements,\nyou will suffer not only when doing HTTP, but with pretty much anything else.\n\nPSR-18 was made for libraries mainly, to avoid dependency conflicts. This does not mean that\nit cannot be used in applications; many people do, and it works!\nWhat it does mean is that its reason to be is to serve libraries, like HTTP SDKS or others. \nIf you are familiar with the whole Guzzle fiasco from a few years ago, you'll\nknow that HTTPPlug (the inspiration for PSR-18) was made with the sole purpose of\nbecoming protection from dependency conflicts in some libraries, mainly caused by a \nvery aggressive release policy from Guzzle, and a very relaxed release policy from Amazon.\n\nSo, the simplicity of this library is more than enough for most of my applications.\n\n### Most HTTP clients are too bloated\n\nAgain, this is not a defect of HTTP clients per se. A client that has many features will \nhave a lot of code and dependencies. The question is whether you need those features for\nyour use case or not. In my experience, most of the time I don't need them, and I\nalways end up doing simple HTTP requests with PHP streams. I built this library so\nI don't have to do that anymore for simple use cases.\n\nAgain, if your use case is more complex, you might want to consider using a more feature\nrich HTTP client. [Symfony Panther](https://github.com/symfony/panther) is my go-to\nrecommendation for web scraping, for example.\n\n### No HTTP client is just a function\n\nOne thing I love about working with javascript is its more functional friendly approach. Even\nthough is light-years away of being a pure functional language, the declarative nature\nof most of it's apis make it really nice to work with (oh if only had proper typing and\nencapsulation!).\n\nThe `fetch` api is one of my favourites, and I always wanted to have something like that in\nPHP. I searched for it, but to no avail, hence this library. Sometimes, most of our single\nmethod classes could perfectly be functions.\n\nSome people in PHP are starting to grasp this and using more functions, especially since\nfunctions can be namespaced now (please never add functions in the global namespace!).\n\n### Immutability\n\nWell, PSR-18 favours immutability in the form of reference clonation. This library does it in\nthe form of read-only state. There is nothing you can change in an already constructed\nresponse. Everything is read only.\n\n\u003e Well, you could change it using nasty php tricks like closure scope binding; but don't\n\u003e do that, okay?\n\nThere is no reason why you should need to change a response from a server. The only thing\nyou can do with the response is compose it into other types: nothing more.\n\n### Other Nit-Pickyness\n\nIt really annoys the freak out of me when a library that implements a protocol does not\nthrow exceptions when a protocol error happens. Like, which SMTP client library gives you\na `Response` containing an error when no target address has been specified instead of throwing\nan exception? How are you supposed to know that an error happened?\n\nThe purpose of implementing a protocol in a language is to mimic the idiosyncrasies of the\nprotocol in the available language constructs. If there is status codes defined for errors, the\nlanguage construct for errors should be used: in this case, an exception should be raised.\n\n[This is one of the biggest problems for me with PSR-18](https://www.php-fig.org/psr/psr-18/#error-handling).\n I think it was a terrible design decision that harms user experience.\n \n### In Closing\n\nThis client is simple, small, functional, immutable, type-safe, well designed and achieves a\ngood balance between protocol strictness and convenience. I think there is nothing like that\nin the PHP ecosystem right now, so there might be a user base for this.\n\nHope you enjoy using it as much as I enjoyed building it.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmnavarrocarter%2Fphp-fetch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmnavarrocarter%2Fphp-fetch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmnavarrocarter%2Fphp-fetch/lists"}