├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── SUPPORT.md ├── dependabot.yml └── workflows │ ├── compatibility-check.yml │ ├── dependabot-auto-merge.yml │ ├── lint.yml │ ├── tests.yml │ └── update-changelog.yml ├── .husky └── pre-commit ├── .phpvmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── Banner.png └── Social.png ├── bin ├── fix.sh ├── lint.sh └── test.sh ├── docs ├── .vitepress │ └── config.mts ├── api │ ├── client-handler.md │ ├── client.md │ ├── content-type-enum.md │ ├── fetch-client.md │ ├── fetch.md │ ├── http-method-helpers.md │ ├── index.md │ ├── method-enum.md │ ├── request.md │ ├── response.md │ └── status-enum.md ├── empty-netlify │ └── .gitkeep ├── examples │ ├── api-integration.md │ ├── async-patterns.md │ ├── authentication.md │ ├── error-handling.md │ ├── file-handling.md │ └── index.md ├── guide │ ├── async-requests.md │ ├── authentication.md │ ├── custom-clients.md │ ├── error-handling.md │ ├── file-uploads.md │ ├── helper-functions.md │ ├── index.md │ ├── installation.md │ ├── logging.md │ ├── making-requests.md │ ├── promise-operations.md │ ├── quickstart.md │ ├── retry-handling.md │ ├── testing.md │ ├── working-with-enums.md │ └── working-with-responses.md ├── index.md ├── map.md └── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── logo.png │ ├── robots.txt │ └── site.webmanifest ├── netlify.toml ├── package-lock.json ├── package.json ├── phpstan.neon ├── phpstan.src.neon.dist ├── pint.json └── src └── Fetch ├── Concerns ├── ConfiguresRequests.php ├── HandlesUris.php ├── ManagesPromises.php ├── ManagesRetries.php └── PerformsHttpRequests.php ├── Enum ├── ContentType.php ├── Method.php └── Status.php ├── Exceptions ├── ClientException.php ├── HttpException.php ├── NetworkException.php └── RequestException.php ├── Http ├── Client.php ├── ClientHandler.php ├── Request.php └── Response.php ├── Interfaces ├── ClientHandler.php └── Response.php ├── Support └── helpers.php └── Traits ├── RequestImmutabilityTrait.php └── ResponseImmutabilityTrait.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: thavarshan 2 | buy_me_a_coffee: thavarshan 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report" 3 | about: "Report something that's broken." 4 | --- 5 | 6 | 7 | 8 | 9 | - Library Version: 1.1.0 10 | - PHP Version: 8.2.0 / 8.3.4 11 | 12 | ### Description 13 | 14 | ### Steps To Reproduce 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request" 3 | about: "Request a new and or additional feature." 4 | --- 5 | 6 | 7 | 8 | 9 | ### Description 10 | 11 | 12 | 13 | ### Feature Details 14 | 15 | 16 | 17 | #### Additional Information 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Help & Support 4 | url: https://github.com/Thavarshan/fetch-php/discussions 5 | about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:' 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | _Describe the problem or feature in addition to a link to the issues._ 4 | 5 | ## Approach 6 | 7 | _How does this change address the problem?_ 8 | 9 | #### Open Questions and Pre-Merge TODOs 10 | 11 | - [ ] Use github checklists. When solved, check the box and explain the answer. 12 | 13 | ## Learning 14 | 15 | _Describe the research stage_ 16 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you discover a security vulnerability within this app, please send an email to (). All security vulnerabilities will and must be promptly addressed. 8 | 9 | ### Public PGP Key 10 | 11 | ``` 12 | -----BEGIN PGP PUBLIC KEY BLOCK----- 13 | Version: Keybase OpenPGP v1.0.0 14 | Comment: https://keybase.io/crypto 15 | 16 | xsBNBGYs38ABCADLaZL4oqilycKBaENfqeBvEnAqOhkh0G1nXm7rusqA+O8lBQBt 17 | TnaXw7Ey9FM9WPqWQ9z4kEtD5qXwjH2y8f6ysAEjziTdmkuw6gnA9hlSnx+X7glg 18 | WQNqYiehgWS4R586XYVYtl0Mkqd8IZpIKXMVw5M7oha2ytrBOliepU6ZykjpvbBS 19 | DGy+9hjtnPNW48kMXR8fTJ1i2h2Zf6nXJCmnZDRs/KxmX4ROFMbRdrTcac2esEPS 20 | JXgqhkSH7eTIFCAaEybX+a/9BreQkI5nn6/+N5fQb4bFh/gmuLs2tMmKtnVzbJw+ 21 | ZFOCpqJaEcAyQaFaanEpGSZB4DotS6N8mQxVABEBAAHNRUplcm9tZSBUaGF5YW5h 22 | bnRoYWpvdGh5IChMYXJhdmVsIEZpbHRlcmFibGUpIDx0anRoYXZhcnNoYW5AZ21h 23 | aWwuY29tPsLAbQQTAQoAFwUCZizfwAIbLwMLCQcDFQoIAh4BAheAAAoJEMsU1RA3 24 | g7Gyw8EH/jpHF418EeeikKeGo1tlEag2aKvNLGYaU3eVhsUl5zxjnM/cLkfxVvEE 25 | /9ZPYkRpoT91aC21UEf7MdgNNM5/qUawtZRkXkSlwSkrFg66YxnzkHNoJLvwcw8R 26 | RQCWOOakc5V8lZwi2fUJBK9jwH2+2X9t4jmFJQ+C80/lG6iOWxbz48lSPXN6uh42 27 | B8TL5h3Vmlw3yFhVAyGIqir5Xlm0MlgFI+yL9IMWgMYA84cbsMpGABcxmmXE4aK5 28 | fukJtQITOOhml+zcyFEaKabrEN7O6GxBOzRI7nX1Tjwk85PrIjjfWvjUi3A4CDPg 29 | oAN8x43uNsFMjzS+SKDslb3/zi/a7l7OwE0EZizfwAEIAMX63ACe1PNJKzBFOqSB 30 | xwRgI4jWhtACWX3kfAlT1vp056GQIwwDqOfUx7cThkBSi85j2/tO3tQUdHHGSmE0 31 | ISdr+C27Ps7zezwDxlnY5AP0vUGITO9tUh6sPmELfgH+zFtiMxfOnTgkED7hab7j 32 | Uuk9xbhZbBM+w1k6uSIrsSSVrFCsSnu0L9kswev1ST9bvae0Cz05h9x7MlpWpnI6 33 | QHCmJF0/P4Dlex4Kae+jkFjkS9DRy1JDNyk+l4rgL5k5zmdTAfHvnrMmQQxwb3Tp 34 | 9RWVSx1or0yyev4+kQxR1B0gfAnlV+5pgxvQV99BY89z+qvg9ympzMroJiSpq18c 35 | uhMAEQEAAcLBhAQYAQoADwUCZizfwAUJDwmcAAIbLgEpCRDLFNUQN4OxssBdIAQZ 36 | AQoABgUCZizfwAAKCRANC2xYqTEKWUcuB/9qEOPmt6o35KDWR7U+Z7JRlFw6RInF 37 | L+pBWxyROehuddhhAaBNQ5XE+D3nQaVW+KFv+nW3PzBsnvMa3FUrRffQVAozEznA 38 | UK+KBdbt+LIGlCXQqPfMLnNVWLj1klWv2Uj7nn2NpO0tBp5638KP+RTaDh12krX2 39 | zUaC15NlCt0+oaNs9eJ9yQdc6UpZUs6wVFJ3jWSbdosov2V7uXc5v7tma9mKVbuZ 40 | hIrfKQc6OMJuyH/15lNxhAmruCB1OZaUQs/dhik5N0UWNa9FboF1x9KfrDhGZm3A 41 | pzx8d+MdQI0Ck44aiar+W4iSiZ2sLPEWdQi6I0tIR8Y5CSsvZ8g3jr57MtMIAIy+ 42 | WK+MHjDi5pJRVLm6xtGYbvuEWRAS3UCuOkHdbtpevpJeaYIavxVzemZQNNVCzcL4 43 | bvt5mmEYXKRdpFyAV2RJIUt1W42mDPSCf1gOI8UhAwpbk7mkVm/MTxw2V6hYpwPm 44 | XZ3d1oHW9Kfyw9Me2ApxeftAVtABZQAwiHRVT4qbimIEUrl55r5zv5SBHvhMBK5O 45 | LR4hUlgfp6lAuCqFISwTxy7fj5GdmDLh/Jd0WzK+bTSibb75h/9Vrnu+h4cS7WXM 46 | qhhR76obVq3D12pcipVHeVnXWTq4pqBpdXt7nGBbjH0Af5gITQ9kJYVRZlfBNkQI 47 | D5A7lvlDuRms5SaY7yPOwE0EZizfwAEIAMh/U2okz9U5pxVvhI0+U0Qmd9BMQtiO 48 | tQWYiiWgIOCcJTpgGghbOsY6iOiQXA8smA9QZwGTKv0b4JWhZXBIckxYv7P/fDoX 49 | q/GibI1s5O+34RATwDeAPneHSyh3rvSutxrM9gj1X0nQnl/NzQN5GDlyPysTwLJu 50 | 69jCXuDst1jwOYHsrbaL4ME6n8CXnlu0kgdvRaSUh9pQA+MqDWqNUVTJq9M66T5H 51 | MyBlaguK1NURXOg9ar+RnEVGoa5gSInxxHzBvn7NENa0EJekJQ++X521MmHyI5Ay 52 | YD+JBLMideRj1Cyunc7KCL7hpnvRgPGQmgTpLNQrlLzHQ7K8ubF9kBkAEQEAAcLB 53 | hAQYAQoADwUCZizfwAUJDwmcAAIbLgEpCRDLFNUQN4OxssBdIAQZAQoABgUCZizf 54 | wAAKCRDO7IpZICfPAVdRB/9JITU20DQSHoXkwXEGA+/+q2Dy7sxd8SX7kOsEc7Ba 55 | h/W5XaodsM03RJYoGceUl9LizXDXKW7w/z/sJnWiZ9JnkeYKLQznZNoWUdTrii+5 56 | dywbPpocTEfnGhT/hug8rgZ34ZGh7WVt02lNdFhfjJ6NPdVpg5w3AieOIhih4cyU 57 | LGPLbuD5GggbYGWProW4Xs8feKxthwXq/PWd3B0uNEsTpsUFMvRvTXbpDCgO/hYP 58 | NZCGyFEXjfn/mN7ZPUgyZxXzMhLAIkVOBq66tAu3mGvqlpnFX5w1s6eT44lXZcUX 59 | XihSgLtkcmmAtHAFxPtba9TZ+K9jhjRBAdzmuIRApzM6secH/3634iES7nDEi0bT 60 | 9gk1oJDgvudWQfHUOO8XxWa06OW/zsqeS7KUyKF1bUc1R9VHovh7c5w/NUvuN9w7 61 | opZnV/aQ0e3BUvdIxxLRbG//8sv+wfP/YsK5+G5AUBNB5PfKfVcyrEZigx2XLyWd 62 | /DV587k8WTR8Zi4cDIeI8aUR4FckqXX3PIkaX2h3KbC2oZtIeDIi6QKnhcNDg92Z 63 | xH1G4bdzcttum/a3j5+pCElcLbTtaqM2SH7BL2ykfpj3F0u+NS/HLzMvvaSbQJwd 64 | j/xDST73sv2oSA1bXG0zZnJAG6gLSA/+wIMTkGpov5g37nFjn8yAwC5f+Dk1IQww 65 | UcKwfSc= 66 | =N/Ig 67 | -----END PGP PUBLIC KEY BLOCK----- 68 | ``` 69 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support Questions 2 | 3 | GitHub issue trackers are not intended to provide help or support. Instead, use one of the following channels: 4 | 5 | - [Twitter](https://twitter.com/tjthavarshan) 6 | - [Github discussions](https://github.com/Thavarshan/fetch-php/issues) 7 | - [API reference](https://github.com/Thavarshan/fetch-php/wiki/API-Reference-for-the-Filter-Class) 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: 'github-actions' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-check.yml: -------------------------------------------------------------------------------- 1 | name: Compatibility Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - refactor/** 8 | - feature/** 9 | - fix/** 10 | 11 | jobs: 12 | check-compatibility: 13 | runs-on: ${{ matrix.os }} 14 | timeout-minutes: 5 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | os: [ubuntu-latest] 19 | php: [8.3, 8.4] 20 | laravel: [9.*, 10.*, 11.*] 21 | 22 | name: PHP ${{ matrix.php }} on ${{ matrix.os }} - Laravel ${{ matrix.laravel }} 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup PHP 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php }} 32 | extensions: curl, mbstring, json, openssl 33 | coverage: none 34 | 35 | - name: Install Composer 36 | run: composer self-update 37 | 38 | - name: Create Laravel project 39 | run: composer create-project --prefer-dist laravel/laravel laravel-project "${{ matrix.laravel }}" --no-progress --no-interaction 40 | 41 | - name: Navigate to Laravel project 42 | working-directory: ./laravel-project 43 | run: | 44 | # Add local package path 45 | composer config repositories.local path ../ 46 | # Require the package 47 | composer require jerome/fetch-php:* --no-update 48 | 49 | - name: Install dependencies 50 | working-directory: ./laravel-project 51 | run: composer update --prefer-dist --no-progress --no-suggest 52 | 53 | - name: Run Laravel tests (if applicable) 54 | working-directory: ./laravel-project 55 | run: php artisan test 56 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: dependabot-auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: '${{ secrets.GITHUB_TOKEN }}' 18 | compat-lookup: true 19 | 20 | - name: Auto-merge Dependabot PRs for semver-minor updates 21 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 22 | run: gh pr merge --auto --merge "$PR_URL" 23 | env: 24 | PR_URL: ${{github.event.pull_request.html_url}} 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | 27 | - name: Auto-merge Dependabot PRs for semver-patch updates 28 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 29 | run: gh pr merge --auto --merge "$PR_URL" 30 | env: 31 | PR_URL: ${{github.event.pull_request.html_url}} 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | 34 | - name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90% 35 | if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}} 36 | run: gh pr merge --auto --merge "$PR_URL" 37 | env: 38 | PR_URL: ${{github.event.pull_request.html_url}} 39 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 40 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | php: [8.4] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php }} 21 | extensions: json, dom, curl, libxml, mbstring 22 | coverage: none 23 | 24 | - name: Run lint 25 | run: composer lint 26 | 27 | # - name: Commit linted files 28 | # uses: stefanzweifel/git-auto-commit-action@v5 29 | # with: 30 | # commit_message: "Fix coding style" 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - refactor/** 8 | - feature/** 9 | - fix/** 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | timeout-minutes: 5 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | os: [ubuntu-latest] 19 | php: [8.3, 8.4] 20 | laravel: [9.*, 10.*, 11.*] 21 | stability: [prefer-stable] 22 | 23 | name: PHP ${{ matrix.php }} on ${{ matrix.os }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }} 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup PHP 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: ${{ matrix.php }} 33 | extensions: curl, mbstring, json, openssl 34 | coverage: none 35 | 36 | - name: Install dependencies 37 | run: | 38 | composer require laravel/framework:${{ matrix.laravel }} --no-update 39 | composer install --no-interaction --no-progress --no-suggest 40 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 41 | 42 | - name: Execute tests 43 | run: composer test 44 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | name: 'Update Changelog' 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | ref: main 16 | token: ${{ secrets.GH_TOKEN }} 17 | 18 | - name: Update Changelog 19 | uses: stefanzweifel/changelog-updater-action@v1 20 | with: 21 | latest-version: ${{ github.event.release.name }} 22 | release-notes: ${{ github.event.release.body }} 23 | 24 | - name: Commit updated CHANGELOG 25 | uses: stefanzweifel/git-auto-commit-action@v5 26 | with: 27 | branch: main 28 | commit_message: Update CHANGELOG 29 | file_pattern: CHANGELOG.md 30 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e # this makes the script fail on first error 4 | 5 | chmod +x bin/fix.sh && ./bin/fix.sh 6 | chmod +x bin/lint.sh && ./bin/lint.sh 7 | php artisan test || true 8 | -------------------------------------------------------------------------------- /.phpvmrc: -------------------------------------------------------------------------------- 1 | 8.2 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "php.version": "8.4" 3 | } 4 | -------------------------------------------------------------------------------- /assets/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/assets/Banner.png -------------------------------------------------------------------------------- /assets/Social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/assets/Social.png -------------------------------------------------------------------------------- /bin/fix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on error and unbound variables 4 | set -eu 5 | 6 | # Function to check if a composer package is installed 7 | is_composer_package_installed() { 8 | composer show "$1" >/dev/null 2>&1 9 | return $? 10 | } 11 | 12 | # Constants 13 | DUSTER_PACKAGE="tightenco/duster" 14 | DUSTER_PATH="vendor/bin/duster" 15 | SRC_DIR="./src" 16 | 17 | # Check if Duster is installed 18 | if ! is_composer_package_installed "$DUSTER_PACKAGE"; then 19 | echo "Installing $DUSTER_PACKAGE..." 20 | composer require --dev "$DUSTER_PACKAGE" 21 | fi 22 | 23 | # Create a timestamp for logs 24 | TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") 25 | LOG_FILE="duster_run_${TIMESTAMP}.log" 26 | 27 | # Check if directories exist before proceeding 28 | if [ ! -d "$SRC_DIR" ]; then 29 | echo "Error: Source directory $SRC_DIR not found!" 30 | exit 1 31 | fi 32 | 33 | # Run the Duster analysis 34 | echo "Running Duster on $SRC_DIR..." 35 | $DUSTER_PATH fix "$SRC_DIR" | tee -a "$LOG_FILE" 36 | 37 | echo "Code formatting completed. Log saved to $LOG_FILE" 38 | 39 | # Create a summary of changes 40 | echo "Summary of changes:" | tee -a "$LOG_FILE" 41 | grep -E "Linting|Fixed|Failed" "$LOG_FILE" | sort | uniq -c | tee -a "$LOG_FILE" 42 | 43 | exit 0 44 | -------------------------------------------------------------------------------- /bin/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit when undeclared variables are used 4 | set -u 5 | 6 | # Make pipe commands return the exit status of the last command that fails 7 | set -o pipefail 8 | 9 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 10 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 11 | cd "$PROJECT_ROOT" 12 | 13 | # Config 14 | DUSTER_PACKAGE="tightenco/duster" 15 | DUSTER_PATH="vendor/bin/duster" 16 | DIRECTORIES_TO_ANALYSE="src" 17 | FIX_MODE=0 18 | AUTO_INSTALL=1 19 | LINT_ONLY=0 20 | STRICT_MODE=0 21 | EXTRA_DIRS="" 22 | VERBOSE=0 23 | 24 | show_usage() { 25 | echo "Usage: $0 [options]" 26 | echo "" 27 | echo "Options:" 28 | echo " -d, --directories DIRS Comma-separated directories (default: app)" 29 | echo " -f, --fix Fix mode" 30 | echo " -l, --lint-only Skip Duster" 31 | echo " -n, --no-install Don't auto-install Duster" 32 | echo " -s, --strict Exit non-zero if any issues found" 33 | echo " -h, --help Show this help" 34 | } 35 | 36 | while [[ $# -gt 0 ]]; do 37 | case $1 in 38 | -d | --directories) 39 | DIRECTORIES_TO_ANALYSE="$2" 40 | shift 2 41 | ;; 42 | --directories=*) 43 | DIRECTORIES_TO_ANALYSE="${1#*=}" 44 | shift 45 | ;; 46 | -f | --fix) 47 | FIX_MODE=1 48 | shift 49 | ;; 50 | -l | --lint-only) 51 | LINT_ONLY=1 52 | shift 53 | ;; 54 | -n | --no-install) 55 | AUTO_INSTALL=0 56 | shift 57 | ;; 58 | -s | --strict) 59 | STRICT_MODE=1 60 | shift 61 | ;; 62 | -v | --verbose) 63 | VERBOSE=1 64 | shift 65 | ;; 66 | -h | --help) 67 | show_usage 68 | exit 0 69 | ;; 70 | *) 71 | if [[ -d "$1" ]]; then 72 | EXTRA_DIRS="$EXTRA_DIRS $1" 73 | else 74 | echo "Unknown option or directory: $1" 75 | show_usage 76 | exit 1 77 | fi 78 | shift 79 | ;; 80 | esac 81 | done 82 | 83 | if [[ -n "$EXTRA_DIRS" ]]; then 84 | DIRECTORIES_TO_ANALYSE="$DIRECTORIES_TO_ANALYSE $EXTRA_DIRS" 85 | fi 86 | 87 | DIRECTORIES_TO_ANALYSE="${DIRECTORIES_TO_ANALYSE//,/ }" 88 | 89 | is_composer_package_installed() { 90 | composer show "$1" >/dev/null 2>&1 91 | } 92 | 93 | validate_php_syntax() { 94 | local directory="$1" 95 | local status=0 96 | local file_count=0 97 | local error_count=0 98 | 99 | echo "Checking PHP syntax in $directory..." 100 | 101 | while IFS= read -r -d $'\0' file; do 102 | file_count=$((file_count + 1)) 103 | if [[ $VERBOSE -eq 1 ]]; then 104 | echo "Checking syntax of $file" 105 | fi 106 | if ! php -l "$file" >/dev/null 2>&1; then 107 | error_count=$((error_count + 1)) 108 | status=1 109 | php -l "$file" 110 | fi 111 | done < <(find "$directory" -type f -name "*.php" -print0) 112 | 113 | echo "✓ Checked $file_count PHP files in $directory with $error_count errors" 114 | return $status 115 | } 116 | 117 | EXIT_STATUS=0 118 | 119 | if [[ $LINT_ONLY -eq 0 ]]; then 120 | if ! is_composer_package_installed "$DUSTER_PACKAGE"; then 121 | if [[ $AUTO_INSTALL -eq 1 ]]; then 122 | echo "Installing $DUSTER_PACKAGE..." 123 | composer require --dev "$DUSTER_PACKAGE" 124 | else 125 | echo "Error: $DUSTER_PACKAGE not installed." 126 | exit 1 127 | fi 128 | fi 129 | 130 | if [[ ! -f "$DUSTER_PATH" ]]; then 131 | echo "Error: Duster binary not found at $DUSTER_PATH" 132 | exit 1 133 | fi 134 | 135 | if [[ $FIX_MODE -eq 1 ]]; then 136 | echo "Running Duster FIX on: $DIRECTORIES_TO_ANALYSE" 137 | $DUSTER_PATH fix $DIRECTORIES_TO_ANALYSE || EXIT_STATUS=1 138 | else 139 | echo "Running Duster LINT on: $DIRECTORIES_TO_ANALYSE" 140 | $DUSTER_PATH lint $DIRECTORIES_TO_ANALYSE || EXIT_STATUS=1 141 | fi 142 | fi 143 | 144 | for dir in $DIRECTORIES_TO_ANALYSE; do 145 | if [[ -d "$dir" ]]; then 146 | if ! validate_php_syntax "$dir"; then 147 | EXIT_STATUS=1 148 | fi 149 | else 150 | echo "Warning: '$dir' not found" 151 | fi 152 | done 153 | 154 | if [[ $EXIT_STATUS -eq 0 ]]; then 155 | echo "✅ All linting checks passed!" 156 | else 157 | echo "⚠️ Linting completed with issues." 158 | fi 159 | 160 | # Only block the commit if strict mode is enabled 161 | if [[ $STRICT_MODE -eq 1 ]]; then 162 | exit $EXIT_STATUS 163 | else 164 | exit 0 165 | fi 166 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make script exit when a command fails 4 | set -e 5 | 6 | # Make script exit when an undeclared variable is used 7 | set -u 8 | 9 | # Make pipe commands return the exit status of the last command that fails or all commands if successful 10 | set -o pipefail 11 | 12 | # Get the directory where the script is located 13 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 14 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 15 | 16 | # Change to project root directory 17 | cd "$PROJECT_ROOT" 18 | 19 | # Variables for customizing test behavior 20 | COVERAGE=0 21 | PARALLEL=0 22 | FILTER="" 23 | SPECIFIC_TEST="" 24 | 25 | # Parse command line arguments 26 | while [[ $# -gt 0 ]]; do 27 | case $1 in 28 | --coverage) 29 | COVERAGE=1 30 | shift 31 | ;; 32 | --parallel) 33 | PARALLEL=1 34 | shift 35 | ;; 36 | --filter=*) 37 | FILTER="${1#*=}" 38 | shift 39 | ;; 40 | --test=*) 41 | SPECIFIC_TEST="${1#*=}" 42 | shift 43 | ;; 44 | --help) 45 | echo "Usage: $0 [options]" 46 | echo "Options:" 47 | echo " --coverage Generate code coverage report" 48 | echo " --parallel Run tests in parallel" 49 | echo " --filter=NAME Only run tests matching the filter" 50 | echo " --test=PATH Run a specific test file or directory" 51 | echo " --help Display this help message" 52 | exit 0 53 | ;; 54 | *) 55 | echo "Unknown option: $1" 56 | echo "Use --help for usage information." 57 | exit 1 58 | ;; 59 | esac 60 | done 61 | 62 | # Check PHP version 63 | PHP_VERSION=$(php -r 'echo PHP_VERSION;') 64 | echo "Using PHP version: $PHP_VERSION" 65 | 66 | # Run composer install if vendor directory is missing 67 | if [ ! -d "vendor" ]; then 68 | echo "Vendor directory missing. Running composer install..." 69 | composer install 70 | fi 71 | 72 | # Build test command 73 | TEST_CMD="vendor/bin/phpunit" 74 | 75 | if [ -n "$FILTER" ]; then 76 | TEST_CMD="$TEST_CMD --filter=$FILTER" 77 | fi 78 | 79 | if [ -n "$SPECIFIC_TEST" ]; then 80 | TEST_CMD="$TEST_CMD $SPECIFIC_TEST" 81 | fi 82 | 83 | if [ "$PARALLEL" -eq 1 ]; then 84 | TEST_CMD="vendor/bin/paratest" 85 | if [ -n "$FILTER" ]; then 86 | echo "Warning: --filter is not supported with parallel testing. Ignoring filter." 87 | fi 88 | fi 89 | 90 | # Run the tests 91 | if [ "$COVERAGE" -eq 1 ]; then 92 | # Check if Xdebug is installed 93 | if php -m | grep -q xdebug; then 94 | echo "Generating test coverage report..." 95 | XDEBUG_MODE=coverage $TEST_CMD --coverage-text --coverage-html=coverage 96 | else 97 | echo "Warning: Xdebug is not installed. Cannot generate coverage report." 98 | $TEST_CMD 99 | fi 100 | else 101 | $TEST_CMD 102 | fi 103 | 104 | # Check exit code 105 | TEST_EXIT_CODE=$? 106 | 107 | # Show summary message 108 | if [ $TEST_EXIT_CODE -eq 0 ]; then 109 | echo "✅ Tests completed successfully!" 110 | else 111 | echo "❌ Tests failed with exit code: $TEST_EXIT_CODE" 112 | fi 113 | 114 | exit $TEST_EXIT_CODE 115 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | title: "Fetch PHP", 8 | description: "A modern HTTP client library that brings JavaScript's fetch API experience to PHP with async/await patterns, promise-based API, and powerful retry mechanics.", 9 | 10 | // IMPORTANT: Set canonical URL base to avoid duplicate content issues 11 | base: '/', 12 | 13 | sitemap: { 14 | hostname: 'https://fetch-php.thavarshan.com' 15 | }, 16 | 17 | // Enable lastUpdated for tags in sitemap 18 | lastUpdated: true, 19 | 20 | head: [ 21 | // Basic meta tags 22 | ['link', { rel: 'icon', href: '/favicon.ico' }], 23 | ['meta', { name: 'author', content: 'Jerome Thayananthajothy' }], 24 | ['meta', { name: 'keywords', content: 'php, http client, fetch api, javascript fetch, guzzle, async php, http requests, promise-based, psr-7, psr-18' }], 25 | 26 | // Open Graph tags for social sharing 27 | ['meta', { property: 'og:type', content: 'website' }], 28 | ['meta', { property: 'og:title', content: 'Fetch PHP - The JavaScript fetch API for PHP' }], 29 | ['meta', { property: 'og:description', content: 'Modern HTTP client for PHP with JavaScript-like syntax, async/await patterns, and powerful retry mechanics.' }], 30 | ['meta', { property: 'og:image', content: 'https://fetch-php.thavarshan.com/og-image.png' }], 31 | ['meta', { property: 'og:url', content: 'https://fetch-php.thavarshan.com' }], 32 | 33 | // Twitter Card tags 34 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 35 | ['meta', { name: 'twitter:title', content: 'Fetch PHP - The JavaScript fetch API for PHP' }], 36 | ['meta', { name: 'twitter:description', content: 'Modern HTTP client for PHP with JavaScript-like syntax, async/await patterns, and powerful retry mechanics.' }], 37 | ['meta', { name: 'twitter:image', content: 'https://fetch-php.thavarshan.com/og-image.png' }], 38 | 39 | // Canonical URL to avoid duplicate content issues 40 | ['link', { rel: 'canonical', href: 'https://fetch-php.thavarshan.com' }], 41 | 42 | // Robots tag to ensure indexing 43 | ['meta', { name: 'robots', content: 'index, follow' }], 44 | 45 | // Preload critical assets 46 | ['link', { rel: 'preload', href: '/logo.png', as: 'image' }], 47 | 48 | // Structured data for rich results (JSON-LD) 49 | ['script', { type: 'application/ld+json' }, `{ 50 | "@context": "https://schema.org", 51 | "@type": "SoftwareApplication", 52 | "name": "Fetch PHP", 53 | "applicationCategory": "DeveloperApplication", 54 | "operatingSystem": "PHP 8.1+", 55 | "offers": { 56 | "@type": "Offer", 57 | "price": "0.00", 58 | "priceCurrency": "USD" 59 | }, 60 | "description": "A modern HTTP client library that brings JavaScript's fetch API experience to PHP with async/await patterns, promise-based API, and powerful retry mechanics.", 61 | "author": { 62 | "@type": "Person", 63 | "name": "Jerome Thayananthajothy" 64 | } 65 | }`] 66 | ], 67 | 68 | // Customize title format for SEO 69 | titleTemplate: '%s | Fetch PHP - Modern HTTP Client', 70 | 71 | // Use clean URLs (no .html extension) 72 | cleanUrls: true, 73 | 74 | themeConfig: { 75 | logo: '/logo.png', 76 | siteTitle: 'Fetch PHP', 77 | 78 | // Improve navigation (keeping your existing structure) 79 | nav: [ 80 | { text: 'Home', link: '/' }, 81 | { text: 'Guide', link: '/guide/' }, 82 | { text: 'API Reference', link: '/api/' }, 83 | { text: 'Examples', link: '/examples/' }, 84 | { text: 'Changelog', link: 'https://github.com/Thavarshan/fetch-php/blob/main/CHANGELOG.md' } 85 | ], 86 | 87 | // Keep your existing sidebar structure 88 | sidebar: { 89 | '/guide/': [ 90 | { 91 | text: 'Introduction', 92 | items: [ 93 | { text: 'Overview', link: '/guide/' }, 94 | { text: 'Installation', link: '/guide/installation' }, 95 | { text: 'Quickstart', link: '/guide/quickstart' } 96 | ] 97 | }, 98 | { 99 | "text": "Core Concepts", 100 | "items": [ 101 | { "text": "Making Requests", "link": "/guide/making-requests" }, 102 | { "text": "Working with Enums", "link": "/guide/working-with-enums" }, 103 | { "text": "Helper Functions", "link": "/guide/helper-functions" }, 104 | { "text": "Working with Responses", "link": "/guide/working-with-responses" }, 105 | { "text": "Authentication", "link": "/guide/authentication" }, 106 | { "text": "Error Handling", "link": "/guide/error-handling" }, 107 | { "text": "Logging", "link": "/guide/logging" } 108 | ] 109 | }, 110 | { 111 | "text": "Advanced Usage", 112 | "items": [ 113 | { "text": "Asynchronous Requests", "link": "/guide/async-requests" }, 114 | { "text": "Promise Operations", "link": "/guide/promise-operations" }, 115 | { "text": "Retry Handling", "link": "/guide/retry-handling" }, 116 | { "text": "File Uploads", "link": "/guide/file-uploads" }, 117 | { "text": "Custom Clients", "link": "/guide/custom-clients" }, 118 | { "text": "Testing with Mocks", "link": "/guide/testing" } 119 | ] 120 | } 121 | ], 122 | '/api/': [ 123 | { 124 | text: 'API Reference', 125 | items: [ 126 | { text: 'Overview', link: '/api/' } 127 | ] 128 | }, 129 | { 130 | text: 'Helper Functions', 131 | items: [ 132 | { text: 'fetch()', link: '/api/fetch' }, 133 | { text: 'fetch_client()', link: '/api/fetch-client' }, 134 | { text: 'HTTP Method Helpers', link: '/api/http-method-helpers' } 135 | ] 136 | }, 137 | { 138 | text: 'Core Classes', 139 | items: [ 140 | { text: 'Client', link: '/api/client' }, 141 | { text: 'ClientHandler', link: '/api/client-handler' }, 142 | { text: 'Request', link: '/api/request' }, 143 | { text: 'Response', link: '/api/response' } 144 | ] 145 | }, 146 | { 147 | text: 'Enums', 148 | items: [ 149 | { text: 'Method', link: '/api/method-enum' }, 150 | { text: 'ContentType', link: '/api/content-type-enum' }, 151 | { text: 'Status', link: '/api/status-enum' } 152 | ] 153 | } 154 | ], 155 | '/examples/': [ 156 | { 157 | text: 'Examples', 158 | items: [ 159 | { text: 'Basic Requests', link: '/examples/' }, 160 | { text: 'Working with APIs', link: '/examples/api-integration' }, 161 | { text: 'Async Request Patterns', link: '/examples/async-patterns' }, 162 | { text: 'Error Handling', link: '/examples/error-handling' }, 163 | { text: 'File Handling', link: '/examples/file-handling' }, 164 | { text: 'Authentication', link: '/examples/authentication' } 165 | ] 166 | } 167 | ] 168 | }, 169 | 170 | // Social links (keeping your GitHub link) 171 | socialLinks: [ 172 | { icon: 'github', link: 'https://github.com/Thavarshan/fetch-php' } 173 | ], 174 | 175 | // Footer content with additional keywords naturally included 176 | footer: { 177 | message: 'Released under the MIT License. A modern HTTP client for PHP developers.', 178 | copyright: 'Copyright © 2024-present Jerome Thayananthajothy' 179 | }, 180 | 181 | // Improved search configuration 182 | search: { 183 | provider: 'local', 184 | options: { 185 | detailedView: true 186 | // Add common misspellings and related terms here if supported in the future 187 | } 188 | }, 189 | 190 | // Configuration for showing the last updated time (freshness signal) 191 | lastUpdated: { 192 | text: 'Updated at', 193 | formatOptions: { 194 | dateStyle: 'medium', 195 | timeStyle: 'short' 196 | } 197 | }, 198 | 199 | // Add edit link to encourage contributions (helps with engagement metrics) 200 | editLink: { 201 | pattern: 'https://github.com/Thavarshan/fetch-php/edit/main/docs/:path', 202 | text: 'Edit this page on GitHub' 203 | }, 204 | 205 | // Add carbon ads if you want to monetize your documentation 206 | // carbonAds: { 207 | // code: 'your-carbon-code', 208 | // placement: 'your-carbon-placement' 209 | // } 210 | }, 211 | buildEnd: async ({ outDir }) => { 212 | const robotsTxt = `User-agent: * 213 | Allow: / 214 | Sitemap: https://fetch-php.thavarshan.com/sitemap.xml`; 215 | 216 | fs.writeFileSync(path.join(outDir, 'robots.txt'), robotsTxt); 217 | console.log('✓ robots.txt generated'); 218 | }, 219 | 220 | // Performance optimizations 221 | vite: { 222 | build: { 223 | minify: true, // Use the default minifier 224 | cssMinify: true 225 | }, 226 | server: { 227 | fs: { 228 | strict: true 229 | } 230 | } 231 | } 232 | }); 233 | -------------------------------------------------------------------------------- /docs/api/client.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client API Reference 3 | description: API reference for the Client class in the Fetch HTTP client package 4 | --- 5 | 6 | # Client API Reference 7 | 8 | The complete API reference for the `Client` class in the Fetch HTTP client package. 9 | 10 | ## Class Declaration 11 | 12 | ```php 13 | namespace Fetch\Http; 14 | 15 | class Client implements ClientInterface, LoggerAwareInterface 16 | { 17 | // ... 18 | } 19 | ``` 20 | 21 | ## Constructor 22 | 23 | ```php 24 | /** 25 | * Client constructor. 26 | * 27 | * @param ClientHandlerInterface|null $handler The client handler 28 | * @param array $options Default request options 29 | * @param LoggerInterface|null $logger PSR-3 logger 30 | */ 31 | public function __construct( 32 | ?ClientHandlerInterface $handler = null, 33 | array $options = [], 34 | ?LoggerInterface $logger = null 35 | ) 36 | ``` 37 | 38 | ## Factory Methods 39 | 40 | ### `createWithBaseUri()` 41 | 42 | Creates a new client with a base URI. 43 | 44 | ```php 45 | public static function createWithBaseUri(string $baseUri, array $options = []): static 46 | ``` 47 | 48 | ## PSR-7 Implementation 49 | 50 | ### `sendRequest()` 51 | 52 | Sends a PSR-7 request and returns a PSR-7 response. Implements the PSR-18 ClientInterface. 53 | 54 | ```php 55 | public function sendRequest(RequestInterface $request): PsrResponseInterface 56 | ``` 57 | 58 | #### Throws 59 | 60 | - `NetworkException` - If there's a network error 61 | - `RequestException` - If there's an error with the request 62 | - `ClientException` - For unexpected errors 63 | 64 | ## Fetch API 65 | 66 | ### `fetch()` 67 | 68 | Creates and sends an HTTP request. Returns the handler for method chaining if no URL is provided. 69 | 70 | ```php 71 | public function fetch(?string $url = null, ?array $options = []): ResponseInterface|ClientHandlerInterface 72 | ``` 73 | 74 | ## HTTP Methods 75 | 76 | ### `get()` 77 | 78 | Makes a GET request. 79 | 80 | ```php 81 | public function get(string $url, ?array $queryParams = null, ?array $options = []): ResponseInterface 82 | ``` 83 | 84 | ### `post()` 85 | 86 | Makes a POST request. 87 | 88 | ```php 89 | public function post( 90 | string $url, 91 | mixed $body = null, 92 | string|ContentType $contentType = ContentType::JSON, 93 | ?array $options = [] 94 | ): ResponseInterface 95 | ``` 96 | 97 | ### `put()` 98 | 99 | Makes a PUT request. 100 | 101 | ```php 102 | public function put( 103 | string $url, 104 | mixed $body = null, 105 | string|ContentType $contentType = ContentType::JSON, 106 | ?array $options = [] 107 | ): ResponseInterface 108 | ``` 109 | 110 | ### `patch()` 111 | 112 | Makes a PATCH request. 113 | 114 | ```php 115 | public function patch( 116 | string $url, 117 | mixed $body = null, 118 | string|ContentType $contentType = ContentType::JSON, 119 | ?array $options = [] 120 | ): ResponseInterface 121 | ``` 122 | 123 | ### `delete()` 124 | 125 | Makes a DELETE request. 126 | 127 | ```php 128 | public function delete( 129 | string $url, 130 | mixed $body = null, 131 | string|ContentType $contentType = ContentType::JSON, 132 | ?array $options = [] 133 | ): ResponseInterface 134 | ``` 135 | 136 | ### `head()` 137 | 138 | Makes a HEAD request. 139 | 140 | ```php 141 | public function head(string $url, ?array $options = []): ResponseInterface 142 | ``` 143 | 144 | ### `options()` 145 | 146 | Makes an OPTIONS request. 147 | 148 | ```php 149 | public function options(string $url, ?array $options = []): ResponseInterface 150 | ``` 151 | 152 | ### `methodRequest()` 153 | 154 | Makes a request with a specific HTTP method (protected method used internally). 155 | 156 | ```php 157 | protected function methodRequest( 158 | Method $method, 159 | string $url, 160 | mixed $body = null, 161 | string|ContentType $contentType = ContentType::JSON, 162 | ?array $options = [] 163 | ): ResponseInterface 164 | ``` 165 | 166 | ## Client Handling 167 | 168 | ### `getHandler()` 169 | 170 | Gets the underlying client handler. 171 | 172 | ```php 173 | public function getHandler(): ClientHandlerInterface 174 | ``` 175 | 176 | ### `getHttpClient()` 177 | 178 | Gets the PSR-7 HTTP client. 179 | 180 | ```php 181 | public function getHttpClient(): ClientInterface 182 | ``` 183 | 184 | ## Logger Integration 185 | 186 | ### `setLogger()` 187 | 188 | Sets a PSR-3 logger. Implements PSR-3 LoggerAwareInterface. 189 | 190 | ```php 191 | public function setLogger(LoggerInterface $logger): void 192 | ``` 193 | 194 | ## Protected Methods 195 | 196 | ### `extractOptionsFromRequest()` 197 | 198 | Extracts options from a PSR-7 request. 199 | 200 | ```php 201 | protected function extractOptionsFromRequest(RequestInterface $request): array 202 | ``` 203 | -------------------------------------------------------------------------------- /docs/api/content-type-enum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ContentType Enum API Reference 3 | description: API reference for the ContentType enum in the Fetch HTTP client package 4 | --- 5 | 6 | # ContentType Enum 7 | 8 | The `ContentType` enum represents common MIME types (content types) used in HTTP requests and responses. It provides type-safe constants for content types and helper methods to work with them. 9 | 10 | ## Namespace 11 | 12 | ```php 13 | namespace Fetch\Enum; 14 | ``` 15 | 16 | ## Definition 17 | 18 | ```php 19 | enum ContentType: string 20 | { 21 | case JSON = 'application/json'; 22 | case FORM_URLENCODED = 'application/x-www-form-urlencoded'; 23 | case MULTIPART = 'multipart/form-data'; 24 | case TEXT = 'text/plain'; 25 | case HTML = 'text/html'; 26 | case XML = 'application/xml'; 27 | case XML_TEXT = 'text/xml'; 28 | case BINARY = 'application/octet-stream'; 29 | case PDF = 'application/pdf'; 30 | case CSV = 'text/csv'; 31 | case ZIP = 'application/zip'; 32 | case JAVASCRIPT = 'application/javascript'; 33 | case CSS = 'text/css'; 34 | 35 | /** 36 | * Get a content type from a string. 37 | * 38 | * @throws \ValueError If the content type is invalid 39 | */ 40 | public static function fromString(string $contentType): self 41 | { 42 | return self::from(strtolower($contentType)); 43 | } 44 | 45 | /** 46 | * Try to get a content type from a string, or return default. 47 | */ 48 | public static function tryFromString(string $contentType, ?self $default = null): ?self 49 | { 50 | return self::tryFrom(strtolower($contentType)) ?? $default; 51 | } 52 | 53 | /** 54 | * Normalize a content type to a ContentType enum value. 55 | */ 56 | public static function normalizeContentType(string|ContentType $contentType): string|ContentType 57 | { 58 | if ($contentType instanceof ContentType) { 59 | return $contentType; 60 | } 61 | // Try to convert to enum without a default 62 | $result = self::tryFromString($contentType); 63 | // Return the enum if found, otherwise return the original string 64 | return $result !== null ? $result : $contentType; 65 | } 66 | 67 | /** 68 | * Check if the content type is JSON. 69 | */ 70 | public function isJson(): bool 71 | { 72 | return $this === self::JSON; 73 | } 74 | 75 | /** 76 | * Check if the content type is a form. 77 | */ 78 | public function isForm(): bool 79 | { 80 | return $this === self::FORM_URLENCODED; 81 | } 82 | 83 | /** 84 | * Check if the content type is multipart. 85 | */ 86 | public function isMultipart(): bool 87 | { 88 | return $this === self::MULTIPART; 89 | } 90 | 91 | /** 92 | * Check if the content type is text-based. 93 | */ 94 | public function isText(): bool 95 | { 96 | return match ($this) { 97 | // These are text-based content types 98 | self::JSON, self::FORM_URLENCODED, self::TEXT, self::HTML, self::XML, self::CSV => true, 99 | // These are binary/non-text content types 100 | self::MULTIPART => false, 101 | // Default for any new enum values added in the future 102 | default => false, 103 | }; 104 | } 105 | } 106 | ``` 107 | 108 | ## Available Constants 109 | 110 | | Constant | Value | Description | 111 | |----------|-------|-------------| 112 | | `ContentType::JSON` | `"application/json"` | JSON format | 113 | | `ContentType::FORM_URLENCODED` | `"application/x-www-form-urlencoded"` | Form URL encoded format (standard form submission) | 114 | | `ContentType::MULTIPART` | `"multipart/form-data"` | Multipart format (for file uploads) | 115 | | `ContentType::TEXT` | `"text/plain"` | Plain text | 116 | | `ContentType::HTML` | `"text/html"` | HTML content | 117 | | `ContentType::XML` | `"application/xml"` | XML format | 118 | | `ContentType::XML_TEXT` | `"text/xml"` | XML in text format | 119 | | `ContentType::BINARY` | `"application/octet-stream"` | Binary data | 120 | | `ContentType::PDF` | `"application/pdf"` | PDF document | 121 | | `ContentType::CSV` | `"text/csv"` | CSV data | 122 | | `ContentType::ZIP` | `"application/zip"` | ZIP archive | 123 | | `ContentType::JAVASCRIPT` | `"application/javascript"` | JavaScript code | 124 | | `ContentType::CSS` | `"text/css"` | CSS stylesheet | 125 | 126 | ## Methods 127 | 128 | ### fromString() 129 | 130 | Converts a string to a ContentType enum value. Throws an exception if the string doesn't match any valid content type. 131 | 132 | ```php 133 | public static function fromString(string $contentType): self 134 | ``` 135 | 136 | **Parameters:** 137 | 138 | - `$contentType`: A string representing a MIME type 139 | 140 | **Returns:** 141 | 142 | - The corresponding ContentType enum value 143 | 144 | **Throws:** 145 | 146 | - `\ValueError` if the string doesn't represent a valid content type 147 | 148 | **Example:** 149 | 150 | ```php 151 | $type = ContentType::fromString('application/json'); // Returns ContentType::JSON 152 | ``` 153 | 154 | ### tryFromString() 155 | 156 | Attempts to convert a string to a ContentType enum value. Returns a default value if the string doesn't match any valid content type. 157 | 158 | ```php 159 | public static function tryFromString(string $contentType, ?self $default = null): ?self 160 | ``` 161 | 162 | **Parameters:** 163 | 164 | - `$contentType`: A string representing a MIME type 165 | - `$default`: The default enum value to return if the string doesn't match (defaults to null) 166 | 167 | **Returns:** 168 | 169 | - The corresponding ContentType enum value or the default value 170 | 171 | **Example:** 172 | 173 | ```php 174 | $type = ContentType::tryFromString('application/json'); // Returns ContentType::JSON 175 | $type = ContentType::tryFromString('invalid/type', ContentType::JSON); // Returns ContentType::JSON 176 | ``` 177 | 178 | ### normalizeContentType() 179 | 180 | Normalizes a content type to a ContentType enum value if possible. If the provided value is already a ContentType enum, it is returned as is. If the string matches a valid content type, the corresponding enum value is returned. Otherwise, the original string is returned. 181 | 182 | ```php 183 | public static function normalizeContentType(string|ContentType $contentType): string|ContentType 184 | ``` 185 | 186 | **Parameters:** 187 | 188 | - `$contentType`: A string or ContentType enum value 189 | 190 | **Returns:** 191 | 192 | - The ContentType enum value if conversion is possible, or the original string 193 | 194 | **Example:** 195 | 196 | ```php 197 | $type = ContentType::normalizeContentType('application/json'); // Returns ContentType::JSON 198 | $type = ContentType::normalizeContentType(ContentType::JSON); // Returns ContentType::JSON 199 | $type = ContentType::normalizeContentType('custom/type'); // Returns 'custom/type' 200 | ``` 201 | 202 | ### isJson() 203 | 204 | Checks if the content type is JSON. 205 | 206 | ```php 207 | public function isJson(): bool 208 | ``` 209 | 210 | **Returns:** 211 | 212 | - `true` if the content type is JSON, `false` otherwise 213 | 214 | **Example:** 215 | 216 | ```php 217 | if ($contentType->isJson()) { 218 | // Handle JSON content 219 | } 220 | ``` 221 | 222 | ### isForm() 223 | 224 | Checks if the content type is form URL encoded. 225 | 226 | ```php 227 | public function isForm(): bool 228 | ``` 229 | 230 | **Returns:** 231 | 232 | - `true` if the content type is form URL encoded, `false` otherwise 233 | 234 | **Example:** 235 | 236 | ```php 237 | if ($contentType->isForm()) { 238 | // Handle form data 239 | } 240 | ``` 241 | 242 | ### isMultipart() 243 | 244 | Checks if the content type is multipart form data. 245 | 246 | ```php 247 | public function isMultipart(): bool 248 | ``` 249 | 250 | **Returns:** 251 | 252 | - `true` if the content type is multipart form data, `false` otherwise 253 | 254 | **Example:** 255 | 256 | ```php 257 | if ($contentType->isMultipart()) { 258 | // Handle multipart form data (file uploads) 259 | } 260 | ``` 261 | 262 | ### isText() 263 | 264 | Checks if the content type is text-based. 265 | 266 | ```php 267 | public function isText(): bool 268 | ``` 269 | 270 | **Returns:** 271 | 272 | - `true` if the content type is text-based (JSON, FORM_URLENCODED, TEXT, HTML, XML, CSV), `false` otherwise 273 | 274 | **Example:** 275 | 276 | ```php 277 | if ($contentType->isText()) { 278 | // Handle text-based content 279 | } else { 280 | // Handle binary content 281 | } 282 | ``` 283 | 284 | ## Usage Examples 285 | 286 | ### With HTTP Requests 287 | 288 | ```php 289 | use Fetch\Enum\ContentType; 290 | 291 | // POST request with JSON 292 | $response = fetch_client()->post( 293 | 'https://api.example.com/users', 294 | ['name' => 'John Doe'], 295 | ContentType::JSON 296 | ); 297 | 298 | // POST request with form data 299 | $response = fetch_client()->post( 300 | 'https://api.example.com/users', 301 | ['name' => 'John Doe'], 302 | ContentType::FORM_URLENCODED 303 | ); 304 | ``` 305 | 306 | ### Setting Content Type Header 307 | 308 | ```php 309 | use Fetch\Enum\ContentType; 310 | 311 | // Using the enum value directly as a header 312 | $client = fetch_client() 313 | ->withHeader('Content-Type', ContentType::JSON->value) 314 | ->post('https://api.example.com/users', $data); 315 | ``` 316 | 317 | ### Using The Normalizer 318 | 319 | ```php 320 | use Fetch\Enum\ContentType; 321 | 322 | // Function that accepts both strings and ContentType enums 323 | function processContent($data, string|ContentType $contentType) { 324 | // Normalize the input content type 325 | $normalizedType = ContentType::normalizeContentType($contentType); 326 | 327 | // Now we can safely check if it's a known enum value 328 | if ($normalizedType instanceof ContentType) { 329 | if ($normalizedType->isJson()) { 330 | return json_encode($data); 331 | } elseif ($normalizedType->isForm()) { 332 | return http_build_query($data); 333 | } 334 | } 335 | 336 | // Handle custom content types 337 | return $data; 338 | } 339 | ``` 340 | 341 | ### Content Type Detection 342 | 343 | ```php 344 | use Fetch\Enum\ContentType; 345 | 346 | function processResponse($response) 347 | { 348 | $contentType = $response->getHeaderLine('Content-Type'); 349 | $parsedType = ContentType::tryFromString($contentType); 350 | 351 | return match($parsedType) { 352 | ContentType::JSON => json_decode($response->getBody(), true), 353 | ContentType::XML => simplexml_load_string($response->getBody()), 354 | ContentType::TEXT, ContentType::HTML => $response->getBody(), 355 | default => throw new RuntimeException("Unsupported content type: {$contentType}") 356 | }; 357 | } 358 | ``` 359 | 360 | ### Working with File Uploads 361 | 362 | ```php 363 | use Fetch\Enum\ContentType; 364 | 365 | // Setting up a multipart file upload 366 | $response = fetch_client()->post( 367 | 'https://api.example.com/upload', 368 | [ 369 | [ 370 | 'name' => 'file', 371 | 'contents' => fopen('/path/to/file.jpg', 'r'), 372 | 'filename' => 'file.jpg' 373 | ], 374 | [ 375 | 'name' => 'description', 376 | 'contents' => 'File description' 377 | ] 378 | ], 379 | ContentType::MULTIPART 380 | ); 381 | ``` 382 | -------------------------------------------------------------------------------- /docs/api/fetch-client.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fetch Client Helper Function API Reference 3 | description: API reference for the fetch_client() helper function in the Fetch HTTP client package 4 | --- 5 | 6 | # fetch_client() Function 7 | 8 | The `fetch_client()` function creates, configures, and returns a global HTTP client instance for making HTTP requests. It provides a singleton-like pattern to maintain a consistent client instance throughout your application. 9 | 10 | ## Signature 11 | 12 | ```php 13 | function fetch_client( 14 | ?array $options = null, 15 | bool $reset = false 16 | ): Client 17 | ``` 18 | 19 | ## Parameters 20 | 21 | | Parameter | Type | Default | Description | 22 | |-----------|------|---------|-------------| 23 | | `$options` | `array\|null` | `null` | Global client options to configure the client. | 24 | | `$reset` | `bool` | `false` | Whether to reset the client instance, creating a new one. | 25 | 26 | ## Return Value 27 | 28 | Returns an instance of the `Fetch\Http\Client` class configured with the provided options. 29 | 30 | ## Throws 31 | 32 | - `RuntimeException` - If client creation or configuration fails 33 | 34 | ## Examples 35 | 36 | ### Basic Usage 37 | 38 | ```php 39 | // Get the global client instance 40 | $client = fetch_client(); 41 | 42 | // Send a GET request 43 | $response = $client->get('https://api.example.com/users'); 44 | ``` 45 | 46 | ### With Configuration Options 47 | 48 | ```php 49 | // Configure the global client with custom options 50 | $client = fetch_client([ 51 | 'base_uri' => 'https://api.example.com', 52 | 'headers' => [ 53 | 'X-API-Key' => 'your-api-key', 54 | 'Accept' => 'application/json' 55 | ], 56 | 'timeout' => 10 57 | ]); 58 | 59 | // Any subsequent call to fetch_client() will use the same configured instance 60 | $sameClient = fetch_client(); 61 | 62 | // Send a request using the configured client 63 | $response = $client->get('/users'); 64 | ``` 65 | 66 | ### Resetting the Client 67 | 68 | ```php 69 | // Get the default client 70 | $client1 = fetch_client(); 71 | 72 | // Configure with specific options 73 | $client2 = fetch_client([ 74 | 'headers' => ['X-Custom' => 'value'] 75 | ]); 76 | 77 | // Create a completely new instance, discarding the previous one 78 | $freshClient = fetch_client(null, true); 79 | ``` 80 | 81 | ### One-liner Requests 82 | 83 | ```php 84 | // Configure and make a request in one line 85 | $response = fetch_client([ 86 | 'base_uri' => 'https://api.example.com', 87 | 'headers' => ['Authorization' => 'Bearer token'] 88 | ])->get('/users'); 89 | 90 | // POST request with JSON data 91 | $response = fetch_client([ 92 | 'base_uri' => 'https://api.example.com' 93 | ])->post('/users', [ 94 | 'name' => 'John Doe', 95 | 'email' => 'john@example.com' 96 | ]); 97 | ``` 98 | 99 | ### Chaining Methods 100 | 101 | ```php 102 | // Chain configuration methods with requests 103 | $response = fetch_client() 104 | ->getHandler() 105 | ->baseUri('https://api.example.com') 106 | ->withHeaders([ 107 | 'X-API-Key' => 'your-api-key', 108 | 'User-Agent' => 'MyApp/1.0' 109 | ]) 110 | ->timeout(5) 111 | ->retry(2) 112 | ->get('/users'); 113 | ``` 114 | 115 | ## Internal Implementation 116 | 117 | Internally, the `fetch_client()` function: 118 | 119 | 1. Maintains a static instance of the `Fetch\Http\Client` class 120 | 2. Creates a new instance when first called or when `$reset` is `true` 121 | 3. Applies the specified options to the client if provided 122 | 4. Creates a new client with the modified handler when options are provided 123 | 5. Returns the configured client ready for use 124 | 125 | The function uses a singleton-like pattern to ensure the same client instance is reused throughout the application, which helps maintain consistent configuration and can improve performance by reusing connections. 126 | 127 | ## Notes 128 | 129 | - The first call to `fetch_client()` creates the singleton instance 130 | - Subsequent calls return the same instance unless `$reset` is `true` 131 | - When providing options to an existing instance, a new instance is created with those options 132 | - The client maintains a separate handler instance that does the actual HTTP work 133 | 134 | ## See Also 135 | 136 | - [fetch()](/api/fetch) - Main function for making HTTP requests 137 | - [HTTP Method Helpers](/api/http-method-helpers) - Specialized helper functions for different HTTP methods 138 | - [Client](/api/client) - More details on the Client class 139 | - [ClientHandler](/api/client-handler) - More details on the underlying client handler implementation 140 | -------------------------------------------------------------------------------- /docs/api/fetch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: fetch() 3 | description: API reference for the fetch() helper function 4 | --- 5 | 6 | # fetch() 7 | 8 | The `fetch()` function is the primary way to make HTTP requests. It's designed to mimic JavaScript's `fetch()` API while providing PHP-specific enhancements. 9 | 10 | ## Signature 11 | 12 | ```php 13 | function fetch( 14 | string|RequestInterface|null $resource = null, 15 | ?array $options = [] 16 | ): ResponseInterface|ClientHandlerInterface|Client 17 | ``` 18 | 19 | ## Parameters 20 | 21 | ### `$resource` 22 | 23 | - Type: `string|RequestInterface|null` 24 | - Default: `null` 25 | 26 | This parameter can be: 27 | 28 | - A URL string to fetch, e.g., `'https://api.example.com/users'` 29 | - A pre-configured `Request` object 30 | - `null` to return the client for method chaining 31 | 32 | ### `$options` 33 | 34 | - Type: `array|null` 35 | - Default: `[]` 36 | 37 | An associative array of request options: 38 | 39 | | Option | Type | Description | 40 | | ------ | ---- | ----------- | 41 | | `method` | `string\|Method` | HTTP method (GET, POST, etc.) | 42 | | `headers` | `array` | Request headers | 43 | | `body` | `mixed` | Request body (raw) | 44 | | `json` | `array` | JSON data to send as body (takes precedence over body) | 45 | | `form` | `array` | Form data to send as body (takes precedence if no json) | 46 | | `multipart` | `array` | Multipart form data (takes precedence if no json/form) | 47 | | `query` | `array` | Query parameters | 48 | | `base_uri` | `string` | Base URI for the request | 49 | | `timeout` | `int` | Request timeout in seconds | 50 | | `retries` | `int` | Number of retries | 51 | | `retry_delay` | `int` | Initial delay between retries in milliseconds | 52 | | `auth` | `array` | Basic auth credentials [username, password] | 53 | | `token` | `string` | Bearer token | 54 | | `proxy` | `string\|array` | Proxy configuration | 55 | | `cookies` | `bool\|CookieJarInterface` | Cookies configuration | 56 | | `allow_redirects` | `bool\|array` | Redirect handling configuration | 57 | | `cert` | `string\|array` | SSL certificate | 58 | | `ssl_key` | `string\|array` | SSL key | 59 | | `stream` | `bool` | Whether to stream the response | 60 | 61 | ## Return Value 62 | 63 | The return value depends on the `$resource` parameter: 64 | 65 | - If `$resource` is `null`: Returns the client instance (`ClientHandlerInterface` or `Client`) for method chaining 66 | - If `$resource` is a URL string: Returns a `ResponseInterface` object 67 | - If `$resource` is a `Request` object: Returns a `ResponseInterface` object 68 | 69 | ## Throws 70 | 71 | - `ClientExceptionInterface` - If a client exception occurs during the request 72 | 73 | ## Examples 74 | 75 | ### Basic GET Request 76 | 77 | ```php 78 | use function Fetch\Http\fetch; 79 | 80 | // Make a simple GET request 81 | $response = fetch('https://api.example.com/users'); 82 | 83 | // Check if the request was successful 84 | if ($response->successful()) { 85 | // Parse the JSON response 86 | $users = $response->json(); 87 | 88 | foreach ($users as $user) { 89 | echo $user['name'] . "\n"; 90 | } 91 | } else { 92 | echo "Error: " . $response->status() . " " . $response->statusText(); 93 | } 94 | ``` 95 | 96 | ### POST Request with JSON Data 97 | 98 | ```php 99 | // Send JSON data 100 | $response = fetch('https://api.example.com/users', [ 101 | 'method' => 'POST', 102 | 'json' => [ 103 | 'name' => 'John Doe', 104 | 'email' => 'john@example.com' 105 | ] 106 | ]); 107 | 108 | // Or use the 'body' option with array (auto-converted to JSON) 109 | $response = fetch('https://api.example.com/users', [ 110 | 'method' => 'POST', 111 | 'body' => ['name' => 'John Doe', 'email' => 'john@example.com'] 112 | ]); 113 | ``` 114 | 115 | ### Setting Headers 116 | 117 | ```php 118 | $response = fetch('https://api.example.com/users', [ 119 | 'headers' => [ 120 | 'Accept' => 'application/json', 121 | 'X-API-Key' => 'your-api-key', 122 | 'User-Agent' => 'MyApp/1.0' 123 | ] 124 | ]); 125 | ``` 126 | 127 | ### Using Query Parameters 128 | 129 | ```php 130 | // Add query parameters 131 | $response = fetch('https://api.example.com/users', [ 132 | 'query' => [ 133 | 'page' => 1, 134 | 'per_page' => 20, 135 | 'sort' => 'created_at', 136 | 'order' => 'desc' 137 | ] 138 | ]); 139 | ``` 140 | 141 | ### Form Submission 142 | 143 | ```php 144 | // Send form data 145 | $response = fetch('https://api.example.com/login', [ 146 | 'method' => 'POST', 147 | 'form' => [ 148 | 'username' => 'johndoe', 149 | 'password' => 'secret', 150 | 'remember' => true 151 | ] 152 | ]); 153 | ``` 154 | 155 | ### File Upload 156 | 157 | ```php 158 | // Upload a file using multipart form data 159 | $response = fetch('https://api.example.com/upload', [ 160 | 'method' => 'POST', 161 | 'multipart' => [ 162 | [ 163 | 'name' => 'file', 164 | 'contents' => file_get_contents('/path/to/file.jpg'), 165 | 'filename' => 'upload.jpg', 166 | 'headers' => ['Content-Type' => 'image/jpeg'] 167 | ], 168 | [ 169 | 'name' => 'description', 170 | 'contents' => 'Profile picture' 171 | ] 172 | ] 173 | ]); 174 | ``` 175 | 176 | ### Authentication 177 | 178 | ```php 179 | // Bearer token authentication 180 | $response = fetch('https://api.example.com/profile', [ 181 | 'token' => 'your-oauth-token' 182 | ]); 183 | 184 | // Basic authentication 185 | $response = fetch('https://api.example.com/protected', [ 186 | 'auth' => ['username', 'password'] 187 | ]); 188 | ``` 189 | 190 | ### Timeouts and Retries 191 | 192 | ```php 193 | // Set timeout and retry options 194 | $response = fetch('https://api.example.com/slow-resource', [ 195 | 'timeout' => 30, // 30 second timeout 196 | 'retries' => 3, // Retry up to 3 times 197 | 'retry_delay' => 100 // Start with 100ms delay (uses exponential backoff) 198 | ]); 199 | ``` 200 | 201 | ### Method Chaining 202 | 203 | ```php 204 | // Return the client for method chaining 205 | $users = fetch() 206 | ->withToken('your-oauth-token') 207 | ->withHeader('Accept', 'application/json') 208 | ->get('https://api.example.com/users') 209 | ->json(); 210 | ``` 211 | 212 | ### Using a Request Object 213 | 214 | ```php 215 | use Fetch\Http\Request; 216 | 217 | // Create a custom request 218 | $request = Request::post('https://api.example.com/users') 219 | ->withJsonBody(['name' => 'John Doe', 'email' => 'john@example.com']) 220 | ->withHeader('X-API-Key', 'your-api-key'); 221 | 222 | // Send the request 223 | $response = fetch($request); 224 | ``` 225 | 226 | ## Internal Implementation 227 | 228 | The `fetch()` function works by: 229 | 230 | 1. Processing the provided options with `process_request_options()` 231 | 2. Handling base URI configuration if provided with `handle_request_with_base_uri()` 232 | 3. Using the global client instance from `fetch_client()` to execute the request 233 | 4. Returning appropriate responses based on the input parameters 234 | 235 | ## Notes 236 | 237 | - The `fetch()` function is not a direct implementation of the Web Fetch API; it's inspired by it but adapted for PHP 238 | - When passing an array as the request body, it's automatically encoded as JSON 239 | - For more complex request scenarios, use method chaining with `fetch()` or the `ClientHandler` class 240 | - The function automatically handles conversion between different data formats based on content type 241 | - When used without arguments, `fetch()` returns the global client instance for method chaining 242 | 243 | ## See Also 244 | 245 | - [fetch_client()](/api/fetch-client) - Get or configure the global client instance 246 | - [HTTP Method Helpers](/api/http-method-helpers) - Specialized helper functions for different HTTP methods 247 | - [ClientHandler](/api/client-handler) - More details on the underlying client implementation 248 | - [Response](/api/response) - API for working with response objects 249 | -------------------------------------------------------------------------------- /docs/api/http-method-helpers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: HTTP Method Helpers API Reference 3 | description: API reference for the HTTP method helpers in the Fetch HTTP client package 4 | --- 5 | 6 | # HTTP Method Helpers 7 | 8 | The Fetch package provides a set of convenient global helper functions for making HTTP requests with different HTTP methods. These helper functions make your code more readable and expressive by providing simplified shortcuts for common operations. 9 | 10 | ## Function Signatures 11 | 12 | ### `get()` 13 | 14 | Perform a GET request. 15 | 16 | ```php 17 | /** 18 | * @param string $url URL to fetch 19 | * @param array|null $query Query parameters 20 | * @param array|null $options Additional request options 21 | * @return ResponseInterface The response 22 | * 23 | * @throws ClientExceptionInterface If a client exception occurs 24 | */ 25 | function get(string $url, ?array $query = null, ?array $options = []): ResponseInterface 26 | ``` 27 | 28 | ### `post()` 29 | 30 | Perform a POST request. 31 | 32 | ```php 33 | /** 34 | * @param string $url URL to fetch 35 | * @param mixed $data Request body or JSON data 36 | * @param array|null $options Additional request options 37 | * @return ResponseInterface The response 38 | * 39 | * @throws ClientExceptionInterface If a client exception occurs 40 | */ 41 | function post(string $url, mixed $data = null, ?array $options = []): ResponseInterface 42 | ``` 43 | 44 | ### `put()` 45 | 46 | Perform a PUT request. 47 | 48 | ```php 49 | /** 50 | * @param string $url URL to fetch 51 | * @param mixed $data Request body or JSON data 52 | * @param array|null $options Additional request options 53 | * @return ResponseInterface The response 54 | * 55 | * @throws ClientExceptionInterface If a client exception occurs 56 | */ 57 | function put(string $url, mixed $data = null, ?array $options = []): ResponseInterface 58 | ``` 59 | 60 | ### `patch()` 61 | 62 | Perform a PATCH request. 63 | 64 | ```php 65 | /** 66 | * @param string $url URL to fetch 67 | * @param mixed $data Request body or JSON data 68 | * @param array|null $options Additional request options 69 | * @return ResponseInterface The response 70 | * 71 | * @throws ClientExceptionInterface If a client exception occurs 72 | */ 73 | function patch(string $url, mixed $data = null, ?array $options = []): ResponseInterface 74 | ``` 75 | 76 | ### `delete()` 77 | 78 | Perform a DELETE request. 79 | 80 | ```php 81 | /** 82 | * @param string $url URL to fetch 83 | * @param mixed $data Request body or JSON data 84 | * @param array|null $options Additional request options 85 | * @return ResponseInterface The response 86 | * 87 | * @throws ClientExceptionInterface If a client exception occurs 88 | */ 89 | function delete(string $url, mixed $data = null, ?array $options = []): ResponseInterface 90 | ``` 91 | 92 | ## Examples 93 | 94 | ### GET Request 95 | 96 | ```php 97 | // Simple GET request 98 | $response = get('https://api.example.com/users'); 99 | 100 | // GET request with query parameters 101 | $response = get('https://api.example.com/users', [ 102 | 'page' => 1, 103 | 'limit' => 10, 104 | 'sort' => 'name' 105 | ]); 106 | 107 | // GET request with additional options 108 | $response = get('https://api.example.com/users', ['page' => 1], [ 109 | 'headers' => [ 110 | 'X-API-Key' => 'your-api-key' 111 | ], 112 | 'timeout' => 5 113 | ]); 114 | 115 | // Process the response 116 | $users = $response->json(); 117 | foreach ($users as $user) { 118 | echo $user['name'] . "\n"; 119 | } 120 | ``` 121 | 122 | ### POST Request 123 | 124 | ```php 125 | // POST request with JSON data 126 | $response = post('https://api.example.com/users', [ 127 | 'name' => 'John Doe', 128 | 'email' => 'john@example.com' 129 | ]); 130 | 131 | // POST request with a string body 132 | $response = post('https://api.example.com/raw', 'Raw request body'); 133 | 134 | // POST request with additional options 135 | $response = post('https://api.example.com/users', 136 | ['name' => 'John Doe'], 137 | [ 138 | 'headers' => [ 139 | 'X-Custom-Header' => 'value' 140 | ], 141 | 'timeout' => 10 142 | ] 143 | ); 144 | 145 | // Check if the request was successful 146 | if ($response->isSuccess()) { 147 | $user = $response->json(); 148 | echo "Created user with ID: " . $user['id']; 149 | } 150 | ``` 151 | 152 | ### PUT Request 153 | 154 | ```php 155 | // PUT request to update a resource 156 | $response = put('https://api.example.com/users/1', [ 157 | 'name' => 'John Doe Updated', 158 | 'email' => 'john.updated@example.com' 159 | ]); 160 | 161 | // Check the response 162 | if ($response->isSuccess()) { 163 | echo "User updated successfully"; 164 | } 165 | ``` 166 | 167 | ### PATCH Request 168 | 169 | ```php 170 | // PATCH request to partially update a resource 171 | $response = patch('https://api.example.com/users/1', [ 172 | 'email' => 'new.email@example.com' 173 | ]); 174 | 175 | // Check the response 176 | if ($response->isSuccess()) { 177 | echo "User email updated successfully"; 178 | } 179 | ``` 180 | 181 | ### DELETE Request 182 | 183 | ```php 184 | // DELETE request to remove a resource 185 | $response = delete('https://api.example.com/users/1'); 186 | 187 | // Delete with request body (for batch deletions) 188 | $response = delete('https://api.example.com/users', [ 189 | 'ids' => [1, 2, 3] 190 | ]); 191 | 192 | // Check if the resource was deleted 193 | if ($response->isSuccess()) { 194 | echo "Resource deleted successfully"; 195 | } 196 | ``` 197 | 198 | ## Internal Implementation 199 | 200 | Internally, these helper functions use the `request_method()` function, which in turn calls the `fetch()` function with the appropriate HTTP method and data configuration: 201 | 202 | ```php 203 | function request_method( 204 | string $method, 205 | string $url, 206 | mixed $data = null, 207 | ?array $options = [], 208 | bool $dataIsQuery = false 209 | ): ResponseInterface 210 | { 211 | $options = $options ?? []; 212 | $options['method'] = $method; 213 | 214 | if ($data !== null) { 215 | if ($dataIsQuery) { 216 | $options['query'] = $data; 217 | } elseif (is_array($data)) { 218 | $options['json'] = $data; // Treat arrays as JSON by default 219 | } else { 220 | $options['body'] = $data; 221 | } 222 | } 223 | 224 | return fetch($url, $options); 225 | } 226 | ``` 227 | 228 | ## Notes 229 | 230 | - These helpers provide a more concise way to make common HTTP requests compared to using `fetch()` directly 231 | - When passing an array as the data parameter in `post()`, `put()`, `patch()`, or `delete()`, it's automatically encoded as JSON 232 | - For GET requests, the data parameter is treated as query parameters 233 | - You can still use the full range of request options by passing them in the `$options` parameter 234 | - All helper functions use the global client instance from `fetch_client()` internally, so any global configuration applies 235 | 236 | ## See Also 237 | 238 | - [fetch()](/api/fetch) - Main function for making HTTP requests 239 | - [fetch_client()](/api/fetch-client) - Get or configure the global client instance 240 | - [Client](/api/client) - More details on the Client class 241 | - [Response](/api/response) - API for working with response objects 242 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Reference for Fetch PHP 3 | description: API reference for the Fetch HTTP client package 4 | --- 5 | 6 | # API Reference 7 | 8 | Welcome to the API reference for the Fetch HTTP client package. This section provides detailed documentation for all the components, functions, classes, and interfaces available in the package. 9 | 10 | ## Core Components 11 | 12 | The Fetch package is built around several key components: 13 | 14 | ### Functions 15 | 16 | - [`fetch()`](./fetch.md) - The primary function for making HTTP requests 17 | - [`fetch_client()`](./fetch-client.md) - Function to create a configured HTTP client 18 | - [HTTP Method Helpers](./http-method-helpers.md) - Helper functions like `get()`, `post()`, etc. 19 | 20 | ### Classes 21 | 22 | - [`Client`](./client.md) - Main HTTP client class 23 | - [`ClientHandler`](./client-handler.md) - Low-level HTTP client implementation 24 | - [`Response`](./response.md) - HTTP response representation 25 | 26 | ### Enums 27 | 28 | - [`Method`](./method-enum.md) - HTTP request methods (GET, POST, PUT, etc.) 29 | - [`ContentType`](./content-type-enum.md) - Content type (MIME type) constants 30 | - [`Status`](./status-enum.md) - HTTP status codes 31 | 32 | ## Architectural Overview 33 | 34 | The Fetch package is designed with a layered architecture: 35 | 36 | 1. **User-facing API**: The `fetch()`, `fetch_client()`, and HTTP method helpers (`get()`, `post()`, etc.) provide a simple, expressive API for common HTTP operations. 37 | 38 | 2. **Client Layer**: The `Client` class provides a higher-level, feature-rich API with method chaining and fluent interface. 39 | 40 | 3. **Handler Layer**: The `ClientHandler` class provides the core HTTP functionality, handling the low-level details of making HTTP requests. 41 | 42 | 4. **HTTP Message Layer**: The `Response` class represents HTTP responses and provides methods for working with them. 43 | 44 | 5. **Utilities and Constants**: Enums (`Method`, `ContentType`, `Status`) and other utilities provide standardized constants and helper functions. 45 | 46 | ## Usage Patterns 47 | 48 | The API is designed to be used in several ways, depending on your needs: 49 | 50 | ### One-line Requests 51 | 52 | ```php 53 | // Quick GET request 54 | $response = fetch('https://api.example.com/users'); 55 | 56 | // Quick POST request with JSON data 57 | $response = fetch('https://api.example.com/users', [ 58 | 'method' => 'POST', 59 | 'json' => [ 60 | 'name' => 'John Doe', 61 | 'email' => 'john@example.com' 62 | ] 63 | ]); 64 | 65 | // Using HTTP method helpers 66 | $response = get('https://api.example.com/users'); 67 | $response = post('https://api.example.com/users', [ 68 | 'name' => 'John Doe', 69 | 'email' => 'john@example.com' 70 | ]); 71 | ``` 72 | 73 | ### Fluent Interface with Client 74 | 75 | ```php 76 | // Create a client with a base URI 77 | $client = fetch_client([ 78 | 'base_uri' => 'https://api.example.com' 79 | ]); 80 | 81 | // Chain method calls for a more complex request 82 | $response = $client 83 | ->getHandler() 84 | ->withHeader('X-API-Key', 'your-api-key') 85 | ->withQueryParameter('page', 1) 86 | ->timeout(5) 87 | ->get('/users'); 88 | ``` 89 | 90 | ### Asynchronous Requests 91 | 92 | ```php 93 | // Make an asynchronous request 94 | $promise = fetch_client() 95 | ->getHandler() 96 | ->async() 97 | ->get('https://api.example.com/users'); 98 | 99 | // Add callbacks 100 | $promise->then( 101 | function ($response) { 102 | // Handle successful response 103 | $users = $response->json(); 104 | foreach ($users as $user) { 105 | echo $user['name'] . "\n"; 106 | } 107 | }, 108 | function ($exception) { 109 | // Handle error 110 | echo "Error: " . $exception->getMessage(); 111 | } 112 | ); 113 | ``` 114 | 115 | ### Request Batching and Concurrency 116 | 117 | ```php 118 | use function Fetch\async; 119 | use function Fetch\await; 120 | use function Fetch\all; 121 | use function Fetch\map; 122 | 123 | // Execute multiple requests in parallel 124 | $results = await(all([ 125 | 'users' => async(fn() => fetch('https://api.example.com/users')), 126 | 'posts' => async(fn() => fetch('https://api.example.com/posts')), 127 | 'comments' => async(fn() => fetch('https://api.example.com/comments')) 128 | ])); 129 | 130 | // Process multiple items with controlled concurrency 131 | $userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 132 | $results = await(map($userIds, function($id) { 133 | return async(function() use ($id) { 134 | return fetch("https://api.example.com/users/{$id}"); 135 | }); 136 | }, 3)); // Process 3 at a time 137 | ``` 138 | 139 | ### Working with Enums 140 | 141 | ```php 142 | use Fetch\Enum\Method; 143 | use Fetch\Enum\ContentType; 144 | use Fetch\Enum\Status; 145 | 146 | // Make a request with enum values 147 | $response = fetch_client() 148 | ->getHandler() 149 | ->withBody($data, ContentType::JSON) 150 | ->sendRequest(Method::POST, 'https://api.example.com/users'); 151 | 152 | // Check response status using enums 153 | if ($response->getStatus() === Status::CREATED) { 154 | echo "User created successfully"; 155 | } elseif ($response->getStatus()->isClientError()) { 156 | echo "Client error: " . $response->getStatus()->phrase(); 157 | } 158 | ``` 159 | 160 | ## Extending the Package 161 | 162 | The package is designed to be extensible. You can: 163 | 164 | - Create custom client handlers 165 | - Extend the base client with additional functionality 166 | - Add middleware for request/response processing 167 | - Create specialized clients for specific APIs 168 | 169 | See the [Custom Clients](../guide/custom-clients.md) guide for more information on extending the package. 170 | 171 | ## Performance Considerations 172 | 173 | - Use the global client instance via `fetch_client()` for best performance, as it reuses connections 174 | - Consider using asynchronous requests for I/O-bound operations 175 | - Use the `map()` function with controlled concurrency for processing multiple items 176 | - For large responses, consider streaming the response with the `stream` option 177 | 178 | ## Compatibility Notes 179 | 180 | - Requires PHP 8.1 or higher 181 | - Built on top of Guzzle HTTP, a widely-used PHP HTTP client 182 | - Follows PSR-7 (HTTP Message Interface) and PSR-18 (HTTP Client) standards 183 | - Supports both synchronous and asynchronous operations 184 | -------------------------------------------------------------------------------- /docs/api/method-enum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Method Enum API Reference 3 | description: API reference for the Method enum in the Fetch HTTP client package 4 | --- 5 | 6 | # Method Enum 7 | 8 | The `Method` enum represents HTTP request methods supported by the Fetch package. It provides type-safe constants for HTTP methods and helper methods to work with them. 9 | 10 | ## Namespace 11 | 12 | ```php 13 | namespace Fetch\Enum; 14 | ``` 15 | 16 | ## Definition 17 | 18 | ```php 19 | enum Method: string 20 | { 21 | case GET = 'GET'; 22 | case POST = 'POST'; 23 | case PUT = 'PUT'; 24 | case PATCH = 'PATCH'; 25 | case DELETE = 'DELETE'; 26 | case HEAD = 'HEAD'; 27 | case OPTIONS = 'OPTIONS'; 28 | 29 | /** 30 | * Get the method from a string. 31 | */ 32 | public static function fromString(string $method): self 33 | { 34 | return self::from(strtoupper($method)); 35 | } 36 | 37 | /** 38 | * Try to get the method from a string, or return default. 39 | */ 40 | public static function tryFromString(string $method, ?self $default = null): ?self 41 | { 42 | return self::tryFrom(strtoupper($method)) ?? $default; 43 | } 44 | 45 | /** 46 | * Determine if the method supports a request body. 47 | */ 48 | public function supportsRequestBody(): bool 49 | { 50 | return in_array($this, [self::POST, self::PUT, self::PATCH, self::DELETE]); 51 | } 52 | } 53 | ``` 54 | 55 | ## Available Constants 56 | 57 | a GET request, but without the response body. | 58 | | `Method::OPTIONS` | `"OPTIONS"` | The OPTIONS method describes the communication options for the target resource. | 59 | | `Method::TRACE` | `"TRACE"` | The TRACE method performs a message loop-back test along the path to the target resource. | 60 | | `Method::CONNECT` | `"CONNECT"` | The CONNECT method establishes a tunnel to the server identified by the target resource. | 61 | 62 | ## Methods 63 | 64 | ### fromString() 65 | 66 | Converts a string to a Method enum value. Throws an exception if the string doesn't match any valid method. 67 | 68 | ```php 69 | public static function fromString(string $method): self 70 | ``` 71 | 72 | **Parameters:** 73 | 74 | - `$method`: A string representing an HTTP method (case-insensitive) 75 | 76 | **Returns:** 77 | 78 | - The corresponding Method enum value 79 | 80 | **Throws:** 81 | 82 | - `\ValueError` if the string doesn't represent a valid HTTP method 83 | 84 | **Example:** 85 | 86 | ```php 87 | $method = Method::fromString('post'); // Returns Method::POST 88 | ``` 89 | 90 | ### tryFromString() 91 | 92 | Attempts to convert a string to a Method enum value. Returns a default value if the string doesn't match any valid method. 93 | 94 | ```php 95 | public static function tryFromString(string $method, ?self $default = null): ?self 96 | ``` 97 | 98 | **Parameters:** 99 | 100 | - `$method`: A string representing an HTTP method (case-insensitive) 101 | - `$default`: The default enum value to return if the string doesn't match (defaults to null) 102 | 103 | **Returns:** 104 | 105 | - The corresponding Method enum value or the default value 106 | 107 | **Example:** 108 | 109 | ```php 110 | $method = Method::tryFromString('post'); // Returns Method::POST 111 | $method = Method::tryFromString('INVALID', Method::GET); // Returns Method::GET 112 | ``` 113 | 114 | ### supportsRequestBody() 115 | 116 | Determines whether the HTTP method supports a request body. 117 | 118 | ```php 119 | public function supportsRequestBody(): bool 120 | ``` 121 | 122 | **Returns:** 123 | 124 | - `true` for POST, PUT, PATCH, and DELETE methods 125 | - `false` for GET, HEAD, and OPTIONS methods 126 | 127 | **Example:** 128 | 129 | ```php 130 | if (Method::POST->supportsRequestBody()) { 131 | // Configure request body 132 | } 133 | ``` 134 | 135 | ## Usage Examples 136 | 137 | ### With ClientHandler 138 | 139 | ```php 140 | use Fetch\Enum\Method; 141 | use Fetch\Http\ClientHandler; 142 | 143 | // Using enum directly 144 | $response = ClientHandler::handle(Method::GET->value, 'https://api.example.com/users'); 145 | 146 | // Checking for request body support 147 | $method = Method::POST; 148 | if ($method->supportsRequestBody()) { 149 | // Configure the request body 150 | } 151 | ``` 152 | 153 | ### Converting from String 154 | 155 | ```php 156 | use Fetch\Enum\Method; 157 | 158 | // From request input (safely handling potential errors) 159 | $methodString = $_POST['method'] ?? 'GET'; 160 | $method = Method::tryFromString($methodString, Method::GET); 161 | 162 | // Converting when you expect the method to be valid 163 | try { 164 | $method = Method::fromString('PATCH'); 165 | } catch (\ValueError $e) { 166 | // Handle invalid method 167 | } 168 | ``` 169 | 170 | ### In Method Selection Logic 171 | 172 | ```php 173 | use Fetch\Enum\Method; 174 | 175 | function processRequest(string $methodString, string $uri, ?array $body): Response 176 | { 177 | $method = Method::tryFromString($methodString, Method::GET); 178 | 179 | return match($method) { 180 | Method::GET => fetch_client()->get($uri), 181 | Method::POST => fetch_client()->post($uri, $body), 182 | Method::PUT => fetch_client()->put($uri, $body), 183 | Method::PATCH => fetch_client()->patch($uri, $body), 184 | Method::DELETE => fetch_client()->delete($uri, $body), 185 | default => throw new InvalidArgumentException("Unsupported method: {$methodString}") 186 | }; 187 | } 188 | ``` 189 | -------------------------------------------------------------------------------- /docs/api/request.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request API Reference 3 | description: API reference for the Request class in the Fetch HTTP client package 4 | --- 5 | 6 | # Request API Reference 7 | 8 | The complete API reference for the `Request` class in the Fetch HTTP client package. 9 | 10 | ## Class Declaration 11 | 12 | ```php 13 | namespace Fetch\Http; 14 | 15 | class Request extends BaseRequest implements RequestInterface 16 | { 17 | use RequestImmutabilityTrait; 18 | 19 | // ... 20 | } 21 | ``` 22 | 23 | ## Constructor 24 | 25 | ```php 26 | /** 27 | * Create a new Request instance. 28 | */ 29 | public function __construct( 30 | string|Method $method, 31 | string|UriInterface $uri, 32 | array $headers = [], 33 | $body = null, 34 | string $version = '1.1', 35 | ?string $requestTarget = null 36 | ) 37 | ``` 38 | 39 | ## Static Factory Methods 40 | 41 | ### HTTP Method Factories 42 | 43 | ### `get()` 44 | 45 | Create a new GET request. 46 | 47 | ```php 48 | public static function get(string|UriInterface $uri, array $headers = []): static 49 | ``` 50 | 51 | ### `post()` 52 | 53 | Create a new POST request. 54 | 55 | ```php 56 | public static function post( 57 | string|UriInterface $uri, 58 | $body = null, 59 | array $headers = [], 60 | ContentType|string|null $contentType = null 61 | ): static 62 | ``` 63 | 64 | ### `put()` 65 | 66 | Create a new PUT request. 67 | 68 | ```php 69 | public static function put( 70 | string|UriInterface $uri, 71 | $body = null, 72 | array $headers = [], 73 | ContentType|string|null $contentType = null 74 | ): static 75 | ``` 76 | 77 | ### `patch()` 78 | 79 | Create a new PATCH request. 80 | 81 | ```php 82 | public static function patch( 83 | string|UriInterface $uri, 84 | $body = null, 85 | array $headers = [], 86 | ContentType|string|null $contentType = null 87 | ): static 88 | ``` 89 | 90 | ### `delete()` 91 | 92 | Create a new DELETE request. 93 | 94 | ```php 95 | public static function delete( 96 | string|UriInterface $uri, 97 | $body = null, 98 | array $headers = [], 99 | ContentType|string|null $contentType = null 100 | ): static 101 | ``` 102 | 103 | ### `head()` 104 | 105 | Create a new HEAD request. 106 | 107 | ```php 108 | public static function head(string|UriInterface $uri, array $headers = []): static 109 | ``` 110 | 111 | ### `options()` 112 | 113 | Create a new OPTIONS request. 114 | 115 | ```php 116 | public static function options(string|UriInterface $uri, array $headers = []): static 117 | ``` 118 | 119 | ### Content Type Factories 120 | 121 | ### `json()` 122 | 123 | Create a new Request instance with a JSON body. 124 | 125 | ```php 126 | public static function json( 127 | string|Method $method, 128 | string|UriInterface $uri, 129 | array $data, 130 | array $headers = [] 131 | ): static 132 | ``` 133 | 134 | ### `form()` 135 | 136 | Create a new Request instance with form parameters. 137 | 138 | ```php 139 | public static function form( 140 | string|Method $method, 141 | string|UriInterface $uri, 142 | array $formParams, 143 | array $headers = [] 144 | ): static 145 | ``` 146 | 147 | ### `multipart()` 148 | 149 | Create a new Request instance with multipart form data. 150 | 151 | ```php 152 | public static function multipart( 153 | string|Method $method, 154 | string|UriInterface $uri, 155 | array $multipart, 156 | array $headers = [] 157 | ): static 158 | ``` 159 | 160 | ## Request Target Methods 161 | 162 | ### `getRequestTarget()` 163 | 164 | Get the request target (path for origin-form, absolute URI for absolute-form, authority for authority-form, or asterisk for asterisk-form). 165 | 166 | ```php 167 | public function getRequestTarget(): string 168 | ``` 169 | 170 | ### `withRequestTarget()` 171 | 172 | Return an instance with the specific request target. 173 | 174 | ```php 175 | public function withRequestTarget($requestTarget): static 176 | ``` 177 | 178 | ## Request Method Information 179 | 180 | ### `getMethodEnum()` 181 | 182 | Get the method as an enum. 183 | 184 | ```php 185 | public function getMethodEnum(): ?Method 186 | ``` 187 | 188 | ### `supportsRequestBody()` 189 | 190 | Check if the request method supports a request body. 191 | 192 | ```php 193 | public function supportsRequestBody(): bool 194 | ``` 195 | 196 | ## Content Type Methods 197 | 198 | ### `getContentTypeEnum()` 199 | 200 | Get the content type from the headers as an enum. 201 | 202 | ```php 203 | public function getContentTypeEnum(): ?ContentType 204 | ``` 205 | 206 | ### `hasJsonContent()` 207 | 208 | Check if the request has JSON content. 209 | 210 | ```php 211 | public function hasJsonContent(): bool 212 | ``` 213 | 214 | ### `hasFormContent()` 215 | 216 | Check if the request has form content. 217 | 218 | ```php 219 | public function hasFormContent(): bool 220 | ``` 221 | 222 | ### `hasMultipartContent()` 223 | 224 | Check if the request has multipart content. 225 | 226 | ```php 227 | public function hasMultipartContent(): bool 228 | ``` 229 | 230 | ### `hasTextContent()` 231 | 232 | Check if the request has text content. 233 | 234 | ```php 235 | public function hasTextContent(): bool 236 | ``` 237 | 238 | ## Body Methods 239 | 240 | ### `getBodyAsString()` 241 | 242 | Get the request body as a string. 243 | 244 | ```php 245 | public function getBodyAsString(): string 246 | ``` 247 | 248 | ### `getBodyAsJson()` 249 | 250 | Get the request body as JSON. 251 | 252 | ```php 253 | public function getBodyAsJson(bool $assoc = true, int $depth = 512, int $options = 0): mixed 254 | ``` 255 | 256 | ### `getBodyAsFormParams()` 257 | 258 | Get the request body as form parameters. 259 | 260 | ```php 261 | public function getBodyAsFormParams(): array 262 | ``` 263 | 264 | ## Request Modification Methods 265 | 266 | ### `withBody()` 267 | 268 | Return an instance with the specified body. 269 | 270 | ```php 271 | public function withBody($body): static 272 | ``` 273 | 274 | ### `withContentType()` 275 | 276 | Set the content type of the request. 277 | 278 | ```php 279 | public function withContentType(ContentType|string $contentType): static 280 | ``` 281 | 282 | ### `withQueryParam()` 283 | 284 | Set a query parameter on the request URI. 285 | 286 | ```php 287 | public function withQueryParam(string $name, string|int|float|bool|null $value): static 288 | ``` 289 | 290 | ### `withQueryParams()` 291 | 292 | Set multiple query parameters on the request URI. 293 | 294 | ```php 295 | public function withQueryParams(array $params): static 296 | ``` 297 | 298 | ### `withBearerToken()` 299 | 300 | Set an authorization header with a bearer token. 301 | 302 | ```php 303 | public function withBearerToken(string $token): static 304 | ``` 305 | 306 | ### `withBasicAuth()` 307 | 308 | Set a basic authentication header. 309 | 310 | ```php 311 | public function withBasicAuth(string $username, string $password): static 312 | ``` 313 | 314 | ### `withJsonBody()` 315 | 316 | Set a JSON body on the request. 317 | 318 | ```php 319 | public function withJsonBody(array $data, int $options = 0): static 320 | ``` 321 | 322 | ### `withFormBody()` 323 | 324 | Set a form body on the request. 325 | 326 | ```php 327 | public function withFormBody(array $data): static 328 | ``` 329 | 330 | ## PSR-7 Methods (from RequestImmutabilityTrait) 331 | 332 | These methods override the PSR-7 request methods to ensure immutability and proper type preservation. 333 | 334 | ### `withAddedHeader()` 335 | 336 | Return an instance with the specified header appended with the given value. 337 | 338 | ```php 339 | public function withAddedHeader($name, $value): static 340 | ``` 341 | 342 | ### `withoutHeader()` 343 | 344 | Return an instance without the specified header. 345 | 346 | ```php 347 | public function withoutHeader($name): static 348 | ``` 349 | 350 | ### `withHeader()` 351 | 352 | Return an instance with the provided value replacing the specified header. 353 | 354 | ```php 355 | public function withHeader($name, $value): static 356 | ``` 357 | 358 | ### `withProtocolVersion()` 359 | 360 | Return an instance with the specified protocol version. 361 | 362 | ```php 363 | public function withProtocolVersion($version): static 364 | ``` 365 | 366 | ### `withUri()` 367 | 368 | Return an instance with the specified URI. 369 | 370 | ```php 371 | public function withUri(UriInterface $uri, $preserveHost = false): static 372 | ``` 373 | 374 | ### `withMethod()` 375 | 376 | Return an instance with the provided HTTP method. 377 | 378 | ```php 379 | public function withMethod($method): static 380 | ``` 381 | -------------------------------------------------------------------------------- /docs/api/response.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Response API Reference 3 | description: API reference for the Response class in the Fetch HTTP client package 4 | --- 5 | 6 | # Response API Reference 7 | 8 | The complete API reference for the `Response` class in the Fetch HTTP client package. 9 | 10 | ## Class Declaration 11 | 12 | ```php 13 | namespace Fetch\Http; 14 | 15 | class Response extends BaseResponse implements ArrayAccess, ResponseInterface 16 | { 17 | use ResponseImmutabilityTrait; 18 | 19 | // ... 20 | } 21 | ``` 22 | 23 | ## Constructor 24 | 25 | ```php 26 | /** 27 | * Create new response instance. 28 | */ 29 | public function __construct( 30 | int|Status $status = Status::OK, 31 | array $headers = [], 32 | string $body = '', 33 | string $version = '1.1', 34 | ?string $reason = null 35 | ) 36 | ``` 37 | 38 | ## Static Factory Methods 39 | 40 | ### `createFromBase()` 41 | 42 | Create a response from a PSR-7 response instance. 43 | 44 | ```php 45 | public static function createFromBase(PsrResponseInterface $response): self 46 | ``` 47 | 48 | ### `withJson()` 49 | 50 | Create a response with JSON content. 51 | 52 | ```php 53 | public static function withJson( 54 | mixed $data, 55 | int|Status $status = Status::OK, 56 | array $headers = [], 57 | int $options = 0 58 | ): self 59 | ``` 60 | 61 | ### `noContent()` 62 | 63 | Create a 204 No Content response. 64 | 65 | ```php 66 | public static function noContent(array $headers = []): self 67 | ``` 68 | 69 | ### `created()` 70 | 71 | Create a 201 Created response with optional JSON body. 72 | 73 | ```php 74 | public static function created( 75 | string $location, 76 | mixed $data = null, 77 | array $headers = [] 78 | ): self 79 | ``` 80 | 81 | ### `withRedirect()` 82 | 83 | Create a redirect response. 84 | 85 | ```php 86 | public static function withRedirect( 87 | string $location, 88 | int|Status $status = Status::FOUND, 89 | array $headers = [] 90 | ): self 91 | ``` 92 | 93 | ## Response Body Methods 94 | 95 | ### `json()` 96 | 97 | Get the body as a JSON-decoded array or object. 98 | 99 | ```php 100 | public function json(bool $assoc = true, bool $throwOnError = true, int $depth = 512, int $options = 0): mixed 101 | ``` 102 | 103 | ### `object()` 104 | 105 | Get the body as a JSON-decoded object. 106 | 107 | ```php 108 | public function object(bool $throwOnError = true): object 109 | ``` 110 | 111 | ### `array()` 112 | 113 | Get the body as a JSON-decoded array. 114 | 115 | ```php 116 | public function array(bool $throwOnError = true): array 117 | ``` 118 | 119 | ### `text()` 120 | 121 | Get the body as plain text. 122 | 123 | ```php 124 | public function text(): string 125 | ``` 126 | 127 | ### `body()` 128 | 129 | Get the raw body content. 130 | 131 | ```php 132 | public function body(): string 133 | ``` 134 | 135 | ### `blob()` 136 | 137 | Get the body as a stream (simulating a "blob" in JavaScript). 138 | 139 | ```php 140 | public function blob() 141 | ``` 142 | 143 | ### `arrayBuffer()` 144 | 145 | Get the body as an array buffer (binary data). 146 | 147 | ```php 148 | public function arrayBuffer(): string 149 | ``` 150 | 151 | ### `xml()` 152 | 153 | Parse the body as XML. 154 | 155 | ```php 156 | public function xml(int $options = 0, bool $throwOnError = true): ?SimpleXMLElement 157 | ``` 158 | 159 | ## Status Code Methods 160 | 161 | ### `status()` 162 | 163 | Get the status code of the response. 164 | 165 | ```php 166 | public function status(): int 167 | ``` 168 | 169 | ### `statusText()` 170 | 171 | Get the status text for the response (e.g., "OK"). 172 | 173 | ```php 174 | public function statusText(): string 175 | ``` 176 | 177 | ### `statusEnum()` 178 | 179 | Get the status as an enum. 180 | 181 | ```php 182 | public function statusEnum(): ?Status 183 | ``` 184 | 185 | ### `isStatus()` 186 | 187 | Check if the response has the given status code. 188 | 189 | ```php 190 | public function isStatus(int|Status $status): bool 191 | ``` 192 | 193 | ## Status Category Methods 194 | 195 | ### `isInformational()` 196 | 197 | Check if the response status code is informational (1xx). 198 | 199 | ```php 200 | public function isInformational(): bool 201 | ``` 202 | 203 | ### `ok()` / `successful()` 204 | 205 | Check if the response status code is a success (2xx). 206 | 207 | ```php 208 | public function ok(): bool 209 | public function successful(): bool 210 | ``` 211 | 212 | ### `isRedirection()` / `redirect()` 213 | 214 | Check if the response status code is a redirection (3xx). 215 | 216 | ```php 217 | public function isRedirection(): bool 218 | public function redirect(): bool 219 | ``` 220 | 221 | ### `isClientError()` / `clientError()` 222 | 223 | Check if the response status code is a client error (4xx). 224 | 225 | ```php 226 | public function isClientError(): bool 227 | public function clientError(): bool 228 | ``` 229 | 230 | ### `isServerError()` / `serverError()` 231 | 232 | Check if the response status code is a server error (5xx). 233 | 234 | ```php 235 | public function isServerError(): bool 236 | public function serverError(): bool 237 | ``` 238 | 239 | ### `failed()` 240 | 241 | Determine if the response is a client or server error. 242 | 243 | ```php 244 | public function failed(): bool 245 | ``` 246 | 247 | ## Specific Status Code Methods 248 | 249 | ### `isOk()` 250 | 251 | Check if the response has a 200 status code. 252 | 253 | ```php 254 | public function isOk(): bool 255 | ``` 256 | 257 | ### `isCreated()` 258 | 259 | Check if the response has a 201 status code. 260 | 261 | ```php 262 | public function isCreated(): bool 263 | ``` 264 | 265 | ### `isAccepted()` 266 | 267 | Check if the response has a 202 status code. 268 | 269 | ```php 270 | public function isAccepted(): bool 271 | ``` 272 | 273 | ### `isNoContent()` 274 | 275 | Check if the response has a 204 status code. 276 | 277 | ```php 278 | public function isNoContent(): bool 279 | ``` 280 | 281 | ### `isMovedPermanently()` 282 | 283 | Check if the response has a 301 status code. 284 | 285 | ```php 286 | public function isMovedPermanently(): bool 287 | ``` 288 | 289 | ### `isFound()` 290 | 291 | Check if the response has a 302 status code. 292 | 293 | ```php 294 | public function isFound(): bool 295 | ``` 296 | 297 | ### `isBadRequest()` 298 | 299 | Check if the response has a 400 status code. 300 | 301 | ```php 302 | public function isBadRequest(): bool 303 | ``` 304 | 305 | ### `isUnauthorized()` 306 | 307 | Check if the response has a 401 status code. 308 | 309 | ```php 310 | public function isUnauthorized(): bool 311 | ``` 312 | 313 | ### `isForbidden()` 314 | 315 | Check if the response has a 403 status code. 316 | 317 | ```php 318 | public function isForbidden(): bool 319 | ``` 320 | 321 | ### `isNotFound()` 322 | 323 | Check if the response has a 404 status code. 324 | 325 | ```php 326 | public function isNotFound(): bool 327 | ``` 328 | 329 | ### `isConflict()` 330 | 331 | Check if the response has a 409 status code. 332 | 333 | ```php 334 | public function isConflict(): bool 335 | ``` 336 | 337 | ### `isUnprocessableEntity()` 338 | 339 | Check if the response has a 422 status code. 340 | 341 | ```php 342 | public function isUnprocessableEntity(): bool 343 | ``` 344 | 345 | ### `isTooManyRequests()` 346 | 347 | Check if the response has a 429 status code. 348 | 349 | ```php 350 | public function isTooManyRequests(): bool 351 | ``` 352 | 353 | ### `isInternalServerError()` 354 | 355 | Check if the response has a 500 status code. 356 | 357 | ```php 358 | public function isInternalServerError(): bool 359 | ``` 360 | 361 | ### `isServiceUnavailable()` 362 | 363 | Check if the response has a 503 status code. 364 | 365 | ```php 366 | public function isServiceUnavailable(): bool 367 | ``` 368 | 369 | ## Header Methods 370 | 371 | ### `headers()` 372 | 373 | Get the headers from the response as an array. 374 | 375 | ```php 376 | public function headers(): array 377 | ``` 378 | 379 | ### `header()` 380 | 381 | Get a specific header from the response. 382 | 383 | ```php 384 | public function header(string $header): ?string 385 | ``` 386 | 387 | ### `hasHeader()` 388 | 389 | Determine if the response contains a specific header. 390 | 391 | ```php 392 | public function hasHeader($header): bool 393 | ``` 394 | 395 | ## Content Type Methods 396 | 397 | ### `contentType()` 398 | 399 | Get the Content-Type header from the response. 400 | 401 | ```php 402 | public function contentType(): ?string 403 | ``` 404 | 405 | ### `contentTypeEnum()` 406 | 407 | Get the Content-Type as an enum. 408 | 409 | ```php 410 | public function contentTypeEnum(): ?ContentType 411 | ``` 412 | 413 | ### `hasJsonContent()` 414 | 415 | Check if the response has JSON content. 416 | 417 | ```php 418 | public function hasJsonContent(): bool 419 | ``` 420 | 421 | ### `hasHtmlContent()` 422 | 423 | Check if the response has HTML content. 424 | 425 | ```php 426 | public function hasHtmlContent(): bool 427 | ``` 428 | 429 | ### `hasTextContent()` 430 | 431 | Check if the response has text content. 432 | 433 | ```php 434 | public function hasTextContent(): bool 435 | ``` 436 | 437 | ## ArrayAccess Implementation 438 | 439 | ### `offsetExists()` 440 | 441 | Determine if the given offset exists in the JSON response. 442 | 443 | ```php 444 | public function offsetExists($offset): bool 445 | ``` 446 | 447 | ### `offsetGet()` 448 | 449 | Get the value at the given offset from the JSON response. 450 | 451 | ```php 452 | public function offsetGet($offset): mixed 453 | ``` 454 | 455 | ### `offsetSet()` 456 | 457 | Set the value at the given offset in the JSON response (unsupported). 458 | 459 | ```php 460 | public function offsetSet($offset, $value): void 461 | ``` 462 | 463 | ### `offsetUnset()` 464 | 465 | Unset the value at the given offset from the JSON response (unsupported). 466 | 467 | ```php 468 | public function offsetUnset($offset): void 469 | ``` 470 | 471 | ## Utility Methods 472 | 473 | ### `get()` 474 | 475 | Get the value for a given key from the JSON response. 476 | 477 | ```php 478 | public function get(string $key, mixed $default = null): mixed 479 | ``` 480 | 481 | ### `__toString()` 482 | 483 | Get the body of the response when converting to string. 484 | 485 | ```php 486 | public function __toString(): string 487 | ``` 488 | 489 | ## PSR-7 Methods (from ResponseImmutabilityTrait) 490 | 491 | These methods override the PSR-7 response methods to ensure immutability and proper type preservation. 492 | 493 | ### `withStatus()` 494 | 495 | Return an instance with the specified status code and reason phrase. 496 | 497 | ```php 498 | public function withStatus($code, $reasonPhrase = ''): static 499 | ``` 500 | 501 | ### `withAddedHeader()` 502 | 503 | Return an instance with the specified header appended with the given value. 504 | 505 | ```php 506 | public function withAddedHeader($name, $value): static 507 | ``` 508 | 509 | ### `withoutHeader()` 510 | 511 | Return an instance without the specified header. 512 | 513 | ```php 514 | public function withoutHeader($name): static 515 | ``` 516 | 517 | ### `withHeader()` 518 | 519 | Return an instance with the provided value replacing the specified header. 520 | 521 | ```php 522 | public function withHeader($name, $value): static 523 | ``` 524 | 525 | ### `withProtocolVersion()` 526 | 527 | Return an instance with the specified protocol version. 528 | 529 | ```php 530 | public function withProtocolVersion($version): static 531 | ``` 532 | 533 | ### `withBody()` 534 | 535 | Return an instance with the specified body. 536 | 537 | ```php 538 | public function withBody(StreamInterface $body): static 539 | ``` 540 | -------------------------------------------------------------------------------- /docs/api/status-enum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Status Enum API Reference 3 | description: API reference for the Status enum in the Fetch HTTP client package 4 | --- 5 | 6 | # Status Enum 7 | 8 | The `Status` enum represents HTTP status codes with their respective descriptions. It provides type-safe constants for HTTP status codes and helper methods to work with them. 9 | 10 | ## Namespace 11 | 12 | ```php 13 | namespace Fetch\Enum; 14 | ``` 15 | 16 | ## Definition 17 | 18 | ```php 19 | enum Status: int 20 | { 21 | // 1xx - Informational 22 | case CONTINUE = 100; 23 | case SWITCHING_PROTOCOLS = 101; 24 | case PROCESSING = 102; 25 | case EARLY_HINTS = 103; 26 | 27 | // 2xx - Success 28 | case OK = 200; 29 | case CREATED = 201; 30 | case ACCEPTED = 202; 31 | case NON_AUTHORITATIVE_INFORMATION = 203; 32 | case NO_CONTENT = 204; 33 | case RESET_CONTENT = 205; 34 | case PARTIAL_CONTENT = 206; 35 | case MULTI_STATUS = 207; 36 | case ALREADY_REPORTED = 208; 37 | case IM_USED = 226; 38 | 39 | // 3xx - Redirection 40 | case MULTIPLE_CHOICES = 300; 41 | case MOVED_PERMANENTLY = 301; 42 | case FOUND = 302; 43 | case SEE_OTHER = 303; 44 | case NOT_MODIFIED = 304; 45 | case USE_PROXY = 305; 46 | case TEMPORARY_REDIRECT = 307; 47 | case PERMANENT_REDIRECT = 308; 48 | 49 | // 4xx - Client Error 50 | case BAD_REQUEST = 400; 51 | case UNAUTHORIZED = 401; 52 | case PAYMENT_REQUIRED = 402; 53 | case FORBIDDEN = 403; 54 | case NOT_FOUND = 404; 55 | case METHOD_NOT_ALLOWED = 405; 56 | case NOT_ACCEPTABLE = 406; 57 | case PROXY_AUTHENTICATION_REQUIRED = 407; 58 | case REQUEST_TIMEOUT = 408; 59 | case CONFLICT = 409; 60 | case GONE = 410; 61 | case LENGTH_REQUIRED = 411; 62 | case PRECONDITION_FAILED = 412; 63 | case PAYLOAD_TOO_LARGE = 413; 64 | case URI_TOO_LONG = 414; 65 | case UNSUPPORTED_MEDIA_TYPE = 415; 66 | case RANGE_NOT_SATISFIABLE = 416; 67 | case EXPECTATION_FAILED = 417; 68 | case IM_A_TEAPOT = 418; 69 | case MISDIRECTED_REQUEST = 421; 70 | case UNPROCESSABLE_ENTITY = 422; 71 | case LOCKED = 423; 72 | case FAILED_DEPENDENCY = 424; 73 | case TOO_EARLY = 425; 74 | case UPGRADE_REQUIRED = 426; 75 | case PRECONDITION_REQUIRED = 428; 76 | case TOO_MANY_REQUESTS = 429; 77 | case REQUEST_HEADER_FIELDS_TOO_LARGE = 431; 78 | case UNAVAILABLE_FOR_LEGAL_REASONS = 451; 79 | 80 | // 5xx - Server Error 81 | case INTERNAL_SERVER_ERROR = 500; 82 | case NOT_IMPLEMENTED = 501; 83 | case BAD_GATEWAY = 502; 84 | case SERVICE_UNAVAILABLE = 503; 85 | case GATEWAY_TIMEOUT = 504; 86 | case HTTP_VERSION_NOT_SUPPORTED = 505; 87 | case VARIANT_ALSO_NEGOTIATES = 506; 88 | case INSUFFICIENT_STORAGE = 507; 89 | case LOOP_DETECTED = 508; 90 | case NOT_EXTENDED = 510; 91 | case NETWORK_AUTHENTICATION_REQUIRED = 511; 92 | 93 | /** 94 | * Get a status code from a string or integer. 95 | * 96 | * @throws \ValueError If the status code is invalid 97 | */ 98 | public static function fromInt(int $statusCode): self 99 | { 100 | return self::from($statusCode); 101 | } 102 | 103 | /** 104 | * Try to get a status code from an integer, or return default. 105 | */ 106 | public static function tryFromInt(int $statusCode, ?self $default = null): ?self 107 | { 108 | return self::tryFrom($statusCode) ?? $default; 109 | } 110 | 111 | /** 112 | * Get the reason phrase for a status code. 113 | */ 114 | public function phrase(): string 115 | { 116 | // Implementation details 117 | } 118 | 119 | /** 120 | * Check if the status code is informational (1xx). 121 | */ 122 | public function isInformational(): bool 123 | { 124 | return $this->value >= 100 && $this->value < 200; 125 | } 126 | 127 | /** 128 | * Check if the status code indicates success (2xx). 129 | */ 130 | public function isSuccess(): bool 131 | { 132 | return $this->value >= 200 && $this->value < 300; 133 | } 134 | 135 | /** 136 | * Check if the status code indicates redirection (3xx). 137 | */ 138 | public function isRedirection(): bool 139 | { 140 | return $this->value >= 300 && $this->value < 400; 141 | } 142 | 143 | /** 144 | * Check if the status code indicates client error (4xx). 145 | */ 146 | public function isClientError(): bool 147 | { 148 | return $this->value >= 400 && $this->value < 500; 149 | } 150 | 151 | /** 152 | * Check if the status code indicates server error (5xx). 153 | */ 154 | public function isServerError(): bool 155 | { 156 | return $this->value >= 500 && $this->value < 600; 157 | } 158 | 159 | /** 160 | * Check if the status code indicates an error (4xx or 5xx). 161 | */ 162 | public function isError(): bool 163 | { 164 | return $this->isClientError() || $this->isServerError(); 165 | } 166 | 167 | /** 168 | * Check if the response is cacheable according to HTTP specifications. 169 | */ 170 | public function isCacheable(): bool 171 | { 172 | return match ($this) { 173 | self::OK, self::NON_AUTHORITATIVE_INFORMATION, self::PARTIAL_CONTENT, 174 | self::MULTIPLE_CHOICES, self::MOVED_PERMANENTLY, self::NOT_FOUND, 175 | self::METHOD_NOT_ALLOWED, self::GONE => true, 176 | default => false, 177 | }; 178 | } 179 | 180 | /** 181 | * Check if the status code indicates that the resource was not modified. 182 | */ 183 | public function isNotModified(): bool 184 | { 185 | return $this === self::NOT_MODIFIED; 186 | } 187 | 188 | /** 189 | * Check if the status code indicates that the resource was empty (204, 304). 190 | */ 191 | public function isEmpty(): bool 192 | { 193 | return $this === self::NO_CONTENT || $this === self::NOT_MODIFIED; 194 | } 195 | } 196 | ``` 197 | 198 | ## Available Constants 199 | 200 | Here's a selection of the most commonly used status codes. For the complete list, refer to the enum definition. 201 | 202 | ### 1xx - Informational 203 | 204 | | Constant | Value | Description | 205 | |----------|-------|-------------| 206 | | `Status::CONTINUE` | 100 | The server has received the request headers and the client should proceed to send the request body. | 207 | | `Status::SWITCHING_PROTOCOLS` | 101 | The requester has asked the server to switch protocols. | 208 | 209 | ### 2xx - Success 210 | 211 | | Constant | Value | Description | 212 | |----------|-------|-------------| 213 | | `Status::OK` | 200 | The request has succeeded. | 214 | | `Status::CREATED` | 201 | The request has been fulfilled and a new resource has been created. | 215 | | `Status::ACCEPTED` | 202 | The request has been accepted for processing, but the processing has not been completed. | 216 | | `Status::NO_CONTENT` | 204 | The server successfully processed the request, but is not returning any content. | 217 | 218 | ### 3xx - Redirection 219 | 220 | | Constant | Value | Description | 221 | |----------|-------|-------------| 222 | | `Status::MOVED_PERMANENTLY` | 301 | The requested resource has been assigned a new permanent URI. | 223 | | `Status::FOUND` | 302 | The resource was found, but at a different URI. | 224 | | `Status::NOT_MODIFIED` | 304 | The resource has not been modified since the last request. | 225 | 226 | ### 4xx - Client Error 227 | 228 | | Constant | Value | Description | 229 | |----------|-------|-------------| 230 | | `Status::BAD_REQUEST` | 400 | The server cannot process the request due to a client error. | 231 | | `Status::UNAUTHORIZED` | 401 | Authentication is required and has failed or has not been provided. | 232 | | `Status::FORBIDDEN` | 403 | The server understood the request but refuses to authorize it. | 233 | | `Status::NOT_FOUND` | 404 | The requested resource could not be found. | 234 | | `Status::METHOD_NOT_ALLOWED` | 405 | The request method is not supported for the requested resource. | 235 | | `Status::TOO_MANY_REQUESTS` | 429 | The user has sent too many requests in a given amount of time. | 236 | 237 | ### 5xx - Server Error 238 | 239 | | Constant | Value | Description | 240 | |----------|-------|-------------| 241 | | `Status::INTERNAL_SERVER_ERROR` | 500 | The server encountered an unexpected condition that prevented it from fulfilling the request. | 242 | | `Status::BAD_GATEWAY` | 502 | The server received an invalid response from an upstream server. | 243 | | `Status::SERVICE_UNAVAILABLE` | 503 | The server is currently unable to handle the request due to temporary overloading or maintenance. | 244 | | `Status::GATEWAY_TIMEOUT` | 504 | The server did not receive a timely response from an upstream server. | 245 | 246 | ## Methods 247 | 248 | ### fromInt() 249 | 250 | Converts an integer status code to a Status enum value. Throws an exception if the code doesn't match any valid status. 251 | 252 | ```php 253 | public static function fromInt(int $statusCode): self 254 | ``` 255 | 256 | **Parameters:** 257 | 258 | - `$statusCode`: An integer representing an HTTP status code 259 | 260 | **Returns:** 261 | 262 | - The corresponding Status enum value 263 | 264 | **Throws:** 265 | 266 | - `\ValueError` if the integer doesn't represent a valid HTTP status code 267 | 268 | **Example:** 269 | 270 | ```php 271 | $status = Status::fromInt(200); // Returns Status::OK 272 | ``` 273 | 274 | ### tryFromInt() 275 | 276 | Attempts to convert an integer to a Status enum value. Returns a default value if the integer doesn't match any valid status. 277 | 278 | ```php 279 | public static function tryFromInt(int $statusCode, ?self $default = null): ?self 280 | ``` 281 | 282 | **Parameters:** 283 | 284 | - `$statusCode`: An integer representing an HTTP status code 285 | - `$default`: The default enum value to return if the integer doesn't match (defaults to null) 286 | 287 | **Returns:** 288 | 289 | - The corresponding Status enum value or the default value 290 | 291 | **Example:** 292 | 293 | ```php 294 | $status = Status::tryFromInt(200); // Returns Status::OK 295 | $status = Status::tryFromInt(999, Status::OK); // Returns Status::OK as fallback 296 | ``` 297 | 298 | ### phrase() 299 | 300 | Returns the standard reason phrase for the status code. 301 | 302 | ```php 303 | public function phrase(): string 304 | ``` 305 | 306 | **Returns:** 307 | 308 | - A string containing the standard reason phrase for the HTTP status code 309 | 310 | **Example:** 311 | 312 | ```php 313 | $text = Status::OK->phrase(); // Returns "OK" 314 | $text = Status::NOT_FOUND->phrase(); // Returns "Not Found" 315 | ``` 316 | 317 | ### isInformational() 318 | 319 | Checks if the status code is in the informational range (100-199). 320 | 321 | ```php 322 | public function isInformational(): bool 323 | ``` 324 | 325 | **Returns:** 326 | 327 | - `true` if the status code is between 100 and 199, `false` otherwise 328 | 329 | **Example:** 330 | 331 | ```php 332 | if (Status::CONTINUE->isInformational()) { 333 | // Handle informational response 334 | } 335 | ``` 336 | 337 | ### isSuccess() 338 | 339 | Checks if the status code is in the success range (200-299). 340 | 341 | ```php 342 | public function isSuccess(): bool 343 | ``` 344 | 345 | **Returns:** 346 | 347 | - `true` if the status code is between 200 and 299, `false` otherwise 348 | 349 | **Example:** 350 | 351 | ```php 352 | if (Status::OK->isSuccess()) { 353 | // Handle successful response 354 | } 355 | ``` 356 | 357 | ### isCacheable() 358 | 359 | Checks if the status code indicates a cacheable response according to HTTP specifications. 360 | 361 | ```php 362 | public function isCacheable(): bool 363 | ``` 364 | 365 | **Returns:** 366 | 367 | - `true` if the status code is considered cacheable (200, 203, 206, 300, 301, 404, 405, 410), `false` otherwise 368 | 369 | **Example:** 370 | 371 | ```php 372 | if ($status->isCacheable()) { 373 | // This response can be cached 374 | } 375 | ``` 376 | 377 | ### isNotModified() 378 | 379 | Checks if the status code indicates the resource has not been modified (304). 380 | 381 | ```php 382 | public function isNotModified(): bool 383 | ``` 384 | 385 | **Returns:** 386 | 387 | - `true` if the status is NOT_MODIFIED (304), `false` otherwise 388 | 389 | **Example:** 390 | 391 | ```php 392 | if ($status->isNotModified()) { 393 | // Use the cached version of the resource 394 | } 395 | ``` 396 | 397 | ### isEmpty() 398 | 399 | Checks if the status code indicates an empty response body (204, 304). 400 | 401 | ```php 402 | public function isEmpty(): bool 403 | ``` 404 | 405 | **Returns:** 406 | 407 | - `true` if the status is NO_CONTENT (204) or NOT_MODIFIED (304), `false` otherwise 408 | 409 | **Example:** 410 | 411 | ```php 412 | if ($status->isEmpty()) { 413 | // Don't attempt to parse response body as it should be empty 414 | } 415 | -------------------------------------------------------------------------------- /docs/empty-netlify/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/empty-netlify/.gitkeep -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Examples 3 | description: Basic examples for using the Fetch HTTP package 4 | --- 5 | 6 | # Basic Examples 7 | 8 | This page provides basic examples of how to use the Fetch HTTP package to make common HTTP requests. 9 | 10 | ## Basic GET Requests 11 | 12 | Making a simple GET request: 13 | 14 | ```php 15 | // Make a GET request 16 | $response = fetch('https://api.example.com/users'); 17 | 18 | // Check if the request was successful 19 | if ($response->successful()) { 20 | // Parse the JSON response 21 | $users = $response->json(); 22 | 23 | // Use the data 24 | foreach ($users as $user) { 25 | echo $user['name'] . "\n"; 26 | } 27 | } else { 28 | echo "Error: " . $response->status() . " " . $response->statusText(); 29 | } 30 | ``` 31 | 32 | Using the `get()` helper function: 33 | 34 | ```php 35 | // Make a GET request with query parameters 36 | $response = get('https://api.example.com/users', [ 37 | 'page' => 1, 38 | 'per_page' => 20, 39 | 'sort' => 'name' 40 | ]); 41 | 42 | // Access data using array syntax 43 | if ($response->successful()) { 44 | echo "Total users: " . $response['meta']['total'] . "\n"; 45 | 46 | foreach ($response['data'] as $user) { 47 | echo "- " . $user['name'] . "\n"; 48 | } 49 | } 50 | ``` 51 | 52 | ## POST Requests 53 | 54 | Creating a resource: 55 | 56 | ```php 57 | // Create a user with JSON data 58 | $response = post('https://api.example.com/users', [ 59 | 'name' => 'John Doe', 60 | 'email' => 'john@example.com', 61 | 'role' => 'admin' 62 | ]); 63 | 64 | // Check for success and get the created resource 65 | if ($response->successful()) { 66 | $user = $response->json(); 67 | echo "Created user with ID: " . $user['id'] . "\n"; 68 | } elseif ($response->isUnprocessableEntity()) { 69 | // Handle validation errors 70 | $errors = $response->json()['errors']; 71 | foreach ($errors as $field => $messages) { 72 | echo "$field: " . implode(', ', $messages) . "\n"; 73 | } 74 | } else { 75 | echo "Error: " . $response->status(); 76 | } 77 | ``` 78 | 79 | Submitting a form: 80 | 81 | ```php 82 | // Send form data 83 | $response = fetch('https://api.example.com/contact', [ 84 | 'method' => 'POST', 85 | 'form' => [ 86 | 'name' => 'Jane Smith', 87 | 'email' => 'jane@example.com', 88 | 'message' => 'Hello, I have a question about your product.' 89 | ] 90 | ]); 91 | 92 | if ($response->successful()) { 93 | echo "Form submitted successfully!"; 94 | } else { 95 | echo "Failed to submit form: " . $response->status(); 96 | } 97 | ``` 98 | 99 | ## PUT and PATCH Requests 100 | 101 | Updating a resource completely (PUT): 102 | 103 | ```php 104 | // Update a user with all fields 105 | $response = put('https://api.example.com/users/123', [ 106 | 'name' => 'John Smith', 107 | 'email' => 'john.smith@example.com', 108 | 'role' => 'editor' 109 | ]); 110 | 111 | if ($response->successful()) { 112 | echo "User updated successfully!"; 113 | } 114 | ``` 115 | 116 | Updating a resource partially (PATCH): 117 | 118 | ```php 119 | // Update only specific fields 120 | $response = patch('https://api.example.com/users/123', [ 121 | 'role' => 'admin' 122 | ]); 123 | 124 | if ($response->successful()) { 125 | echo "User role updated successfully!"; 126 | } 127 | ``` 128 | 129 | ## DELETE Requests 130 | 131 | Deleting a resource: 132 | 133 | ```php 134 | // Delete a user 135 | $response = delete('https://api.example.com/users/123'); 136 | 137 | if ($response->successful()) { 138 | echo "User deleted successfully!"; 139 | } elseif ($response->isNotFound()) { 140 | echo "User not found!"; 141 | } else { 142 | echo "Failed to delete user: " . $response->status(); 143 | } 144 | ``` 145 | 146 | Bulk delete with a request body: 147 | 148 | ```php 149 | // Delete multiple users 150 | $response = delete('https://api.example.com/users', [ 151 | 'ids' => [123, 456, 789] 152 | ]); 153 | 154 | if ($response->successful()) { 155 | $result = $response->json(); 156 | echo "Deleted " . $result['deleted_count'] . " users"; 157 | } 158 | ``` 159 | 160 | ## Working with Headers 161 | 162 | Adding custom headers: 163 | 164 | ```php 165 | // Send a request with custom headers 166 | $response = fetch('https://api.example.com/data', [ 167 | 'headers' => [ 168 | 'X-API-Version' => '2.0', 169 | 'Accept-Language' => 'en-US', 170 | 'Cache-Control' => 'no-cache', 171 | 'X-Request-ID' => uniqid() 172 | ] 173 | ]); 174 | 175 | // Get response headers 176 | $contentType = $response->header('Content-Type'); 177 | $rateLimitRemaining = $response->header('X-RateLimit-Remaining'); 178 | 179 | echo "Content Type: $contentType\n"; 180 | echo "Rate Limit Remaining: $rateLimitRemaining\n"; 181 | ``` 182 | 183 | ## Request Timeout 184 | 185 | Setting request timeout: 186 | 187 | ```php 188 | // Set a 5-second timeout 189 | $response = fetch('https://api.example.com/slow-operation', [ 190 | 'timeout' => 5 191 | ]); 192 | 193 | // Or with the fluent interface 194 | $response = fetch() 195 | ->timeout(5) 196 | ->get('https://api.example.com/slow-operation'); 197 | ``` 198 | 199 | ## Working with Different Response Types 200 | 201 | ### JSON Responses 202 | 203 | ```php 204 | $response = get('https://api.example.com/users/123'); 205 | 206 | // Get as associative array (default) 207 | $userData = $response->json(); 208 | echo $userData['name']; 209 | 210 | // Get as object 211 | $userObject = $response->object(); 212 | echo $userObject->name; 213 | 214 | // Using array access syntax directly on response 215 | $name = $response['name']; 216 | $email = $response['email']; 217 | ``` 218 | 219 | ### XML Responses 220 | 221 | ```php 222 | $response = get('https://api.example.com/feed', null, [ 223 | 'headers' => ['Accept' => 'application/xml'] 224 | ]); 225 | 226 | // Parse XML 227 | $xml = $response->xml(); 228 | 229 | // Work with SimpleXMLElement 230 | foreach ($xml->item as $item) { 231 | echo (string)$item->title . "\n"; 232 | echo (string)$item->description . "\n"; 233 | } 234 | ``` 235 | 236 | ### Raw Responses 237 | 238 | ```php 239 | // Get raw response body (for non-JSON/XML content) 240 | $response = get('https://api.example.com/text-content'); 241 | $content = $response->body(); 242 | 243 | // Or using text() method 244 | $text = $response->text(); 245 | 246 | echo "Content length: " . strlen($content) . " bytes"; 247 | ``` 248 | 249 | ## Configuring Global Defaults 250 | 251 | Setting global configuration for all requests: 252 | 253 | ```php 254 | // Configure once at application bootstrap 255 | fetch_client([ 256 | 'base_uri' => 'https://api.example.com', 257 | 'timeout' => 10, 258 | 'headers' => [ 259 | 'User-Agent' => 'MyApp/1.0', 260 | 'Accept' => 'application/json' 261 | ] 262 | ]); 263 | 264 | // Now use simplified calls in your code 265 | $users = get('/users')->json(); // Uses base_uri 266 | $user = post('/users', ['name' => 'John'])->json(); 267 | ``` 268 | 269 | ## Method Chaining 270 | 271 | Using the fluent interface: 272 | 273 | ```php 274 | $response = fetch() 275 | ->withHeader('X-API-Key', 'your-api-key') 276 | ->withQueryParam('include', 'comments,likes') 277 | ->withQueryParam('sort', 'created_at') 278 | ->timeout(5) 279 | ->get('https://api.example.com/posts'); 280 | 281 | if ($response->successful()) { 282 | $posts = $response->json(); 283 | // Process posts 284 | } 285 | ``` 286 | 287 | ## Next Steps 288 | 289 | Now that you're familiar with the basics, check out these more advanced examples: 290 | 291 | - [API Integration Examples](/examples/api-integration) - Real-world API integration patterns 292 | - [Async Patterns](/examples/async-patterns) - Working with asynchronous requests 293 | - [Error Handling](/examples/error-handling) - Robust error handling strategies 294 | - [File Handling](/examples/file-handling) - Uploading and downloading files 295 | - [Authentication](/examples/authentication) - Working with different authentication schemes 296 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | description: An overview of the Fetch HTTP package, its architecture and main components 4 | --- 5 | 6 | # Fetch HTTP Package - Overview 7 | 8 | ## Introduction 9 | 10 | The Fetch HTTP package provides a modern, flexible HTTP client for PHP applications, bringing JavaScript's `fetch` API experience to PHP. It features a fluent interface, extensive configuration options, and robust error handling, making it ideal for consuming APIs and working with web services. 11 | 12 | ## Key Components 13 | 14 | The package is comprised of several main components: 15 | 16 | ### Client 17 | 18 | The `Client` class is a high-level wrapper that: 19 | 20 | - Implements PSR-18 ClientInterface for standardized HTTP client behavior 21 | - Implements PSR-3 LoggerAwareInterface for easy integration with logging systems 22 | - Provides a simple fetch-style API similar to JavaScript's fetch API 23 | - Handles error conversion to specific exception types 24 | 25 | ### ClientHandler 26 | 27 | The `ClientHandler` class is a more powerful, configurable implementation that: 28 | 29 | - Offers a fluent, chainable API for request building 30 | - Provides extensive configuration options for request customization 31 | - Supports both synchronous and asynchronous requests 32 | - Implements retry logic with exponential backoff 33 | - Offers promise-based operations for complex async workflows 34 | 35 | ### Response 36 | 37 | The `Response` class extends PSR-7 ResponseInterface and provides: 38 | 39 | - Rich methods for working with HTTP responses 40 | - Status code helpers like `isOk()`, `isNotFound()`, etc. 41 | - Content type inspection with `hasJsonContent()`, `hasHtmlContent()`, etc. 42 | - Convenient data access methods like `json()`, `text()`, `array()`, `object()` 43 | - Array access interface for working with JSON responses 44 | 45 | ### Enums 46 | 47 | Type-safe PHP 8.1 enums for HTTP concepts: 48 | 49 | - `Method`: HTTP methods (GET, POST, PUT, etc.) 50 | - `ContentType`: Content types with helpers like `isJson()`, `isText()` 51 | - `Status`: HTTP status codes with helpers like `isSuccess()`, `isClientError()` 52 | 53 | ## Feature Highlights 54 | 55 | - **JavaScript-like API**: Familiar syntax for developers coming from JavaScript 56 | - **PSR Compatibility**: Implements PSR-18 (HTTP Client), PSR-7 (HTTP Messages), and PSR-3 (Logger) 57 | - **Fluent Interface**: Chain method calls for clean, readable code 58 | - **Type-Safe Enums**: Modern PHP 8.1 enums for HTTP methods, content types, and status codes 59 | - **Flexible Authentication**: Support for Bearer tokens, Basic auth, and more 60 | - **Logging**: Comprehensive request/response logging with sanitization of sensitive data 61 | - **Retries**: Configurable retry logic with exponential backoff and jitter 62 | - **Asynchronous Requests**: Promise-based async operations with concurrency control 63 | - **Content Type Handling**: Simplified handling of JSON, forms, multipart data, etc. 64 | - **Testing Utilities**: Built-in mock response helpers for testing 65 | 66 | ## Architecture 67 | 68 | ``` 69 | +------------------+ 70 | | Client | <-- High-level API (PSR-18 compliant) 71 | +------------------+ 72 | | 73 | v 74 | +------------------+ 75 | | ClientHandler | <-- Core implementation with advanced features 76 | +------------------+ 77 | | 78 | +-----------+ 79 | | Traits | <-- Functionality separated into focused traits 80 | +-----------+ 81 | | ConfiguresRequests 82 | | HandlesUris 83 | | ManagesPromises 84 | | ManagesRetries 85 | | PerformsHttpRequests 86 | +-----------+ 87 | | 88 | v 89 | +------------------+ 90 | | Response | <-- Enhanced response handling 91 | +------------------+ 92 | | 93 | v 94 | +------------------+ 95 | | Guzzle Client | <-- Underlying HTTP client implementation 96 | +------------------+ 97 | ``` 98 | 99 | ## Usage Patterns 100 | 101 | ### Simple Usage 102 | 103 | ```php 104 | // Global helper functions for quick requests 105 | $response = fetch('https://api.example.com/users'); 106 | $users = $response->json(); 107 | 108 | // HTTP method-specific helpers 109 | $user = post('https://api.example.com/users', [ 110 | 'name' => 'John Doe', 111 | 'email' => 'john@example.com' 112 | ])->json(); 113 | ``` 114 | 115 | ### Advanced Configuration 116 | 117 | The `ClientHandler` class offers more control and customization options for advanced use cases: 118 | 119 | ```php 120 | use Fetch\Http\ClientHandler; 121 | 122 | $handler = new ClientHandler(); 123 | $response = $handler 124 | ->withToken('your-api-token') 125 | ->withHeaders(['Accept' => 'application/json']) 126 | ->withQueryParameters(['page' => 1, 'limit' => 10]) 127 | ->timeout(5) 128 | ->retry(3, 100) 129 | ->get('https://api.example.com/users'); 130 | ``` 131 | 132 | ### Type-Safe Enums 133 | 134 | ```php 135 | use Fetch\Enum\Method; 136 | use Fetch\Enum\ContentType; 137 | use Fetch\Enum\Status; 138 | 139 | // Use enums for HTTP methods 140 | $client = fetch_client(); 141 | $response = $client->request(Method::POST, '/users', $userData); 142 | 143 | // Check HTTP status with enums 144 | if ($response->statusEnum() === Status::OK) { 145 | // Process successful response 146 | } 147 | 148 | // Content type handling 149 | $response = $client->withBody($data, ContentType::JSON)->post('/users'); 150 | ``` 151 | 152 | ### Asynchronous Requests 153 | 154 | For handling multiple requests efficiently: 155 | 156 | ```php 157 | use function async; 158 | use function await; 159 | use function all; 160 | 161 | // Execute an async function 162 | await(async(function() { 163 | // Create multiple requests 164 | $results = await(all([ 165 | 'users' => async(fn() => fetch('https://api.example.com/users')), 166 | 'posts' => async(fn() => fetch('https://api.example.com/posts')), 167 | 'comments' => async(fn() => fetch('https://api.example.com/comments')) 168 | ])); 169 | 170 | // Process the results 171 | $users = $results['users']->json(); 172 | $posts = $results['posts']->json(); 173 | $comments = $results['comments']->json(); 174 | 175 | echo "Fetched " . count($users) . " users, " . 176 | count($posts) . " posts, and " . 177 | count($comments) . " comments"; 178 | })); 179 | ``` 180 | 181 | ### Global Client Configuration 182 | 183 | ```php 184 | // Configure once at application bootstrap 185 | fetch_client([ 186 | 'base_uri' => 'https://api.example.com', 187 | 'headers' => [ 188 | 'User-Agent' => 'MyApp/1.0', 189 | 'Accept' => 'application/json', 190 | ], 191 | 'timeout' => 10, 192 | ]); 193 | 194 | // Use the configured client throughout your application 195 | function getUserData($userId) { 196 | return fetch_client()->get("/users/{$userId}")->json(); 197 | } 198 | 199 | function createUser($userData) { 200 | return fetch_client()->post('/users', $userData)->json(); 201 | } 202 | ``` 203 | 204 | ## Enhanced Response Handling 205 | 206 | ```php 207 | $response = fetch('https://api.example.com/users/1'); 208 | 209 | // Status code helpers 210 | if ($response->isOk()) { 211 | // Handle 200 OK 212 | } else if ($response->isNotFound()) { 213 | // Handle 404 Not Found 214 | } else if ($response->isUnauthorized()) { 215 | // Handle 401 Unauthorized 216 | } 217 | 218 | // Status category helpers 219 | if ($response->successful()) { 220 | // Handle any 2xx status 221 | } else if ($response->isClientError()) { 222 | // Handle any 4xx status 223 | } else if ($response->isServerError()) { 224 | // Handle any 5xx status 225 | } 226 | 227 | // Content type helpers 228 | if ($response->hasJsonContent()) { 229 | $data = $response->json(); 230 | } else if ($response->hasHtmlContent()) { 231 | $html = $response->text(); 232 | } 233 | 234 | // Array access for JSON responses 235 | $user = $response['user']; 236 | $name = $response['user']['name']; 237 | ``` 238 | 239 | ## When to Use Each Class 240 | 241 | ### Use `Client` when 242 | 243 | - You need PSR-18 compatibility 244 | - You prefer a simpler API similar to JavaScript's fetch 245 | - You're working within a framework that expects a PSR-18 client 246 | - You want built-in exception handling for network and HTTP errors 247 | 248 | ### Use `ClientHandler` when 249 | 250 | - You need advanced configuration options 251 | - You want to use asynchronous requests and promises 252 | - You need fine-grained control over retries and timeouts 253 | - You're performing complex operations like concurrent requests 254 | 255 | ### Use global helpers (`fetch()`, `get()`, `post()`, etc.) when 256 | 257 | - You're making simple, one-off requests 258 | - You don't need extensive configuration 259 | - You want the most concise, readable code 260 | 261 | ## Exception Handling 262 | 263 | The package provides several exception types for different error scenarios: 264 | 265 | - `NetworkException`: For connection and network-related errors 266 | - `RequestException`: For HTTP request errors 267 | - `ClientException`: For unexpected client errors 268 | - `TimeoutException`: For request timeouts 269 | 270 | Each exception provides context about the failed request to aid in debugging and error handling. 271 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: How to install the Fetch HTTP package in your PHP project 4 | --- 5 | 6 | **# Installation** 7 | 8 | The Fetch HTTP package can be installed using Composer, the PHP dependency manager. 9 | 10 | **## Requirements** 11 | 12 | - PHP 8.1 or higher 13 | - Composer 14 | 15 | **## Install via Composer** 16 | 17 | **### 1. Add to your project** 18 | 19 | ```bash 20 | composer require jerome/fetch-php 21 | ``` 22 | 23 | **### 2. Update your autoloader** 24 | 25 | If you haven't already done so, make sure you include the Composer autoloader in your project: 26 | 27 | ```php 28 | require __DIR__ . '/vendor/autoload.php'; 29 | ``` 30 | 31 | **## Manual Installation (Not Recommended)** 32 | 33 | While we strongly recommend using Composer, you can also manually download the package and include it in your project: 34 | 35 | 1. Download the latest release from [GitHub](https://github.com/Thavarshan/fetch-php/releases) 36 | 2. Extract the files into your project directory 37 | 3. Set up your own autoloading system or include files manually 38 | 39 | **## Verifying Installation** 40 | 41 | After installation, you can verify that everything is working correctly by creating a simple script: 42 | 43 | ```php 44 | successful()) { 52 | echo "Installation successful!\n"; 53 | echo "Response status: " . $response->status() . "\n"; 54 | echo "Response body: " . $response->body() . "\n"; 55 | } else { 56 | echo "Something went wrong. HTTP status: " . $response->status() . "\n"; 57 | } 58 | ``` 59 | 60 | **## Testing Different Response Methods** 61 | 62 | You can also test some of the enhanced response methods: 63 | 64 | ```php 65 | hasJsonContent()) { 73 | echo "Received JSON content\n"; 74 | 75 | // Parse the JSON data 76 | $data = $response->json(); 77 | echo "JSON data successfully parsed\n"; 78 | 79 | // Access data using array syntax 80 | if (isset($response['slideshow'])) { 81 | echo "Slideshow title: " . $response['slideshow']['title'] . "\n"; 82 | } 83 | } else { 84 | echo "Did not receive JSON content\n"; 85 | } 86 | 87 | // Test status code helpers 88 | if ($response->isOk()) { 89 | echo "Response has 200 OK status\n"; 90 | } 91 | ``` 92 | 93 | **## Next Steps** 94 | 95 | After installation, check out the [Quickstart](/guide/quickstart) guide to begin using the Fetch HTTP package. 96 | -------------------------------------------------------------------------------- /docs/guide/quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart 3 | description: Get up and running with the Fetch HTTP package quickly 4 | --- 5 | 6 | # Quickstart 7 | 8 | This guide will help you get started with the Fetch HTTP package quickly. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | composer require jerome/fetch-php 14 | ``` 15 | 16 | ## Basic Usage 17 | 18 | The Fetch HTTP package provides a simple, intuitive API for making HTTP requests, inspired by JavaScript's fetch API but built specifically for PHP. 19 | 20 | ### Making a Simple Request 21 | 22 | ```php 23 | // Make a GET request using the global fetch() function 24 | $response = fetch('https://api.example.com/users'); 25 | 26 | // Parse the JSON response 27 | $users = $response->json(); 28 | 29 | // Check for success 30 | if ($response->successful()) { 31 | foreach ($users as $user) { 32 | echo $user['name'] . PHP_EOL; 33 | } 34 | } else { 35 | echo "Request failed with status: " . $response->status(); 36 | } 37 | ``` 38 | 39 | ### HTTP Methods 40 | 41 | The package provides helper functions for common HTTP methods: 42 | 43 | ```php 44 | // GET request 45 | $users = get('https://api.example.com/users')->json(); 46 | 47 | // POST request with JSON body 48 | $user = post('https://api.example.com/users', [ 49 | 'name' => 'John Doe', 50 | 'email' => 'john@example.com' 51 | ])->json(); 52 | 53 | // PUT request to update a resource 54 | $updatedUser = put('https://api.example.com/users/123', [ 55 | 'name' => 'John Smith' 56 | ])->json(); 57 | 58 | // PATCH request for partial updates 59 | $user = patch('https://api.example.com/users/123', [ 60 | 'status' => 'active' 61 | ])->json(); 62 | 63 | // DELETE request 64 | $result = delete('https://api.example.com/users/123')->json(); 65 | ``` 66 | 67 | ### Request Options 68 | 69 | The `fetch()` function accepts various options to customize your request: 70 | 71 | ```php 72 | $response = fetch('https://api.example.com/users', [ 73 | 'method' => 'POST', 74 | 'headers' => [ 75 | 'Authorization' => 'Bearer your-token', 76 | 'Accept' => 'application/json', 77 | 'X-Custom-Header' => 'value' 78 | ], 79 | 'json' => [ 80 | 'name' => 'John Doe', 81 | 'email' => 'john@example.com' 82 | ], 83 | 'query' => [ 84 | 'include' => 'posts,comments', 85 | 'sort' => 'created_at' 86 | ], 87 | 'timeout' => 10, 88 | 'retries' => 3 89 | ]); 90 | ``` 91 | 92 | ### Working with Responses 93 | 94 | The `Response` object provides methods for different content types: 95 | 96 | ```php 97 | // JSON response 98 | $data = $response->json(); // As array 99 | $object = $response->object(); // As object 100 | $array = $response->array(); // Explicitly as array 101 | 102 | // Plain text response 103 | $text = $response->text(); 104 | 105 | // Raw response body 106 | $content = $response->body(); 107 | 108 | // Binary data 109 | $binary = $response->arrayBuffer(); 110 | $stream = $response->blob(); 111 | 112 | // XML response 113 | $xml = $response->xml(); 114 | ``` 115 | 116 | You can also access JSON data using array syntax: 117 | 118 | ```php 119 | $name = $response['name']; 120 | $email = $response['email']; 121 | $address = $response['address']['street']; 122 | ``` 123 | 124 | ### Checking Response Status 125 | 126 | ```php 127 | // Status code checks 128 | if ($response->isOk()) { // 200 129 | // Success 130 | } elseif ($response->isNotFound()) { // 404 131 | // Not found 132 | } elseif ($response->isUnauthorized()) { // 401 133 | // Unauthorized 134 | } 135 | 136 | // Status categories 137 | if ($response->successful()) { // 2xx 138 | // Success 139 | } elseif ($response->isClientError()) { // 4xx 140 | // Client error 141 | } elseif ($response->isServerError()) { // 5xx 142 | // Server error 143 | } 144 | ``` 145 | 146 | ### Using Enums 147 | 148 | ```php 149 | use Fetch\Enum\Status; 150 | use Fetch\Enum\Method; 151 | use Fetch\Enum\ContentType; 152 | 153 | // Check status using enum 154 | if ($response->statusEnum() === Status::OK) { 155 | // Status is 200 OK 156 | } 157 | 158 | // Use method enum for requests 159 | $response = fetch_client()->request(Method::POST, '/users', $userData); 160 | 161 | // Use content type enum 162 | $response = fetch_client() 163 | ->withBody($data, ContentType::JSON) 164 | ->post('/users'); 165 | ``` 166 | 167 | ## Authentication 168 | 169 | ### Bearer Token 170 | 171 | ```php 172 | // Using fetch options 173 | $response = fetch('https://api.example.com/users', [ 174 | 'token' => 'your-token' 175 | ]); 176 | 177 | // Using helper methods 178 | $response = fetch_client() 179 | ->withToken('your-token') 180 | ->get('https://api.example.com/users'); 181 | ``` 182 | 183 | ### Basic Authentication 184 | 185 | ```php 186 | // Using fetch options 187 | $response = fetch('https://api.example.com/users', [ 188 | 'auth' => ['username', 'password'] 189 | ]); 190 | 191 | // Using helper methods 192 | $response = fetch_client() 193 | ->withAuth('username', 'password') 194 | ->get('https://api.example.com/users'); 195 | ``` 196 | 197 | ## Advanced Features 198 | 199 | ### Global Configuration 200 | 201 | Set up global configuration for all requests: 202 | 203 | ```php 204 | fetch_client([ 205 | 'base_uri' => 'https://api.example.com', 206 | 'headers' => [ 207 | 'User-Agent' => 'MyApp/1.0', 208 | 'Accept' => 'application/json' 209 | ], 210 | 'timeout' => 5 211 | ]); 212 | 213 | // Now all requests use this configuration 214 | $users = get('/users')->json(); // Uses base_uri 215 | $user = get("/users/{$id}")->json(); 216 | ``` 217 | 218 | ### File Uploads 219 | 220 | ```php 221 | $response = fetch('https://api.example.com/upload', [ 222 | 'method' => 'POST', 223 | 'multipart' => [ 224 | [ 225 | 'name' => 'file', 226 | 'contents' => file_get_contents('/path/to/file.jpg'), 227 | 'filename' => 'upload.jpg', 228 | ], 229 | [ 230 | 'name' => 'description', 231 | 'contents' => 'File description' 232 | ] 233 | ] 234 | ]); 235 | 236 | // Or using the fluent interface 237 | $response = fetch_client() 238 | ->withMultipart([ 239 | [ 240 | 'name' => 'file', 241 | 'contents' => fopen('/path/to/file.jpg', 'r'), 242 | 'filename' => 'upload.jpg', 243 | ], 244 | [ 245 | 'name' => 'description', 246 | 'contents' => 'File description' 247 | ] 248 | ]) 249 | ->post('https://api.example.com/upload'); 250 | ``` 251 | 252 | ### Asynchronous Requests 253 | 254 | ```php 255 | use function async; 256 | use function await; 257 | use function all; 258 | 259 | // Modern async/await pattern 260 | await(async(function() { 261 | // Process multiple requests in parallel 262 | $results = await(all([ 263 | 'users' => async(fn() => fetch('https://api.example.com/users')), 264 | 'posts' => async(fn() => fetch('https://api.example.com/posts')), 265 | 'comments' => async(fn() => fetch('https://api.example.com/comments')) 266 | ])); 267 | 268 | // Process the results 269 | $users = $results['users']->json(); 270 | $posts = $results['posts']->json(); 271 | $comments = $results['comments']->json(); 272 | 273 | echo "Fetched " . count($users) . " users, " . 274 | count($posts) . " posts, and " . 275 | count($comments) . " comments"; 276 | })); 277 | 278 | // Traditional promise pattern 279 | $handler = fetch_client()->getHandler(); 280 | $handler->async(); 281 | 282 | $promise = $handler->get('https://api.example.com/users') 283 | ->then(function ($response) { 284 | if ($response->successful()) { 285 | return $response->json(); 286 | } 287 | throw new \Exception("Request failed with status: " . $response->getStatusCode()); 288 | }) 289 | ->catch(function (\Throwable $e) { 290 | echo "Error: " . $e->getMessage(); 291 | }); 292 | ``` 293 | 294 | ### Retry Handling 295 | 296 | ```php 297 | // Retry failed requests automatically 298 | $response = fetch('https://api.example.com/unstable', [ 299 | 'retries' => 3, // Retry up to 3 times 300 | 'retry_delay' => 100 // Start with 100ms delay (uses exponential backoff) 301 | ]); 302 | 303 | // Or using the fluent interface 304 | $response = fetch_client() 305 | ->retry(3, 100) 306 | ->retryStatusCodes([408, 429, 500, 502, 503, 504]) // Customize retryable status codes 307 | ->retryExceptions(['GuzzleHttp\Exception\ConnectException']) // Customize retryable exceptions 308 | ->get('https://api.example.com/unstable'); 309 | ``` 310 | 311 | ### Content Type Detection 312 | 313 | ```php 314 | $response = fetch('https://api.example.com/data'); 315 | 316 | // Detect content type 317 | $contentType = $response->contentType(); 318 | $contentTypeEnum = $response->contentTypeEnum(); 319 | 320 | // Check specific content types 321 | if ($response->hasJsonContent()) { 322 | $data = $response->json(); 323 | } elseif ($response->hasHtmlContent()) { 324 | $html = $response->text(); 325 | } elseif ($response->hasTextContent()) { 326 | $text = $response->text(); 327 | } 328 | ``` 329 | 330 | ### Working with URIs 331 | 332 | ```php 333 | $response = fetch_client() 334 | ->baseUri('https://api.example.com') 335 | ->withQueryParameter('page', 1) 336 | ->withQueryParameters([ 337 | 'limit' => 10, 338 | 'sort' => 'name', 339 | 'include' => 'posts,comments' 340 | ]) 341 | ->get('/users'); 342 | ``` 343 | 344 | ### Logging 345 | 346 | ```php 347 | use Monolog\Logger; 348 | use Monolog\Handler\StreamHandler; 349 | 350 | // Create a PSR-3 compatible logger 351 | $logger = new Logger('http'); 352 | $logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG)); 353 | 354 | // Set it on the client 355 | $client = fetch_client(); 356 | $client->setLogger($logger); 357 | 358 | // Or set it on the handler 359 | $handler = fetch_client()->getHandler(); 360 | $handler->setLogger($logger); 361 | 362 | // Now all requests will be logged 363 | $response = get('https://api.example.com/users'); 364 | ``` 365 | 366 | ### Creating Mock Responses (For Testing) 367 | 368 | ```php 369 | use Fetch\Http\ClientHandler; 370 | 371 | // Create a simple mock response 372 | $mockResponse = ClientHandler::createMockResponse( 373 | statusCode: 200, 374 | headers: ['Content-Type' => 'application/json'], 375 | body: '{"name": "John", "email": "john@example.com"}' 376 | ); 377 | 378 | // Create a JSON mock response 379 | $mockJsonResponse = ClientHandler::createJsonResponse( 380 | data: ['name' => 'John', 'email' => 'john@example.com'], 381 | statusCode: 200 382 | ); 383 | ``` 384 | 385 | ## Error Handling 386 | 387 | ```php 388 | try { 389 | $response = fetch('https://api.example.com/users'); 390 | 391 | if ($response->failed()) { 392 | throw new Exception("Request failed with status: " . $response->status()); 393 | } 394 | 395 | $users = $response->json(); 396 | } catch (\Fetch\Exceptions\NetworkException $e) { 397 | echo "Network error: " . $e->getMessage(); 398 | } catch (\Fetch\Exceptions\RequestException $e) { 399 | echo "Request error: " . $e->getMessage(); 400 | } catch (\Fetch\Exceptions\ClientException $e) { 401 | echo "Client error: " . $e->getMessage(); 402 | } catch (Exception $e) { 403 | echo "Error: " . $e->getMessage(); 404 | } 405 | ``` 406 | 407 | ## Next Steps 408 | 409 | Now that you've gotten started with the basic functionality, check out these guides to learn more: 410 | 411 | - [Making Requests](/guide/making-requests) - More details on making HTTP requests 412 | - [Helper Functions](/guide/helper-functions) - Learn about all available helper functions 413 | - [Working with Responses](/guide/working-with-responses) - Advanced response handling 414 | - [Working with Enums](/guide/working-with-enums) - Using type-safe enums for HTTP concepts 415 | -------------------------------------------------------------------------------- /docs/guide/retry-handling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Retry Handling 3 | description: Learn how to configure automatic retry behavior for failed HTTP requests 4 | --- 5 | 6 | # Retry Handling 7 | 8 | The Fetch PHP package includes built-in retry functionality to handle transient failures gracefully. This guide explains how to configure and use the retry mechanism. 9 | 10 | ## Basic Retry Configuration 11 | 12 | You can configure retries using the `retry()` method on the ClientHandler: 13 | 14 | ```php 15 | use Fetch\Http\ClientHandler; 16 | 17 | $response = ClientHandler::create() 18 | ->retry(3, 100) // Retry up to 3 times with initial delay of 100ms 19 | ->get('https://api.example.com/unstable-endpoint'); 20 | ``` 21 | 22 | Using helper functions: 23 | 24 | ```php 25 | $response = fetch('https://api.example.com/unstable-endpoint', [ 26 | 'retries' => 3, // Retry up to 3 times 27 | 'retry_delay' => 100 // Initial delay of 100ms 28 | ]); 29 | ``` 30 | 31 | ## How Retry Works 32 | 33 | When a request fails due to a retryable error (network issues or certain HTTP status codes), the package will: 34 | 35 | 1. Wait for a specified delay 36 | 2. Apply exponential backoff with jitter (randomness) 37 | 3. Retry the request 38 | 4. Repeat until success or the maximum retry count is reached 39 | 40 | The delay increases exponentially with each retry attempt: 41 | 42 | - First retry: Initial delay (e.g., 100ms) 43 | - Second retry: ~2x initial delay + jitter 44 | - Third retry: ~4x initial delay + jitter 45 | - And so on... 46 | 47 | The jitter (random variation) helps prevent multiple clients from retrying simultaneously, which can worsen outages. 48 | 49 | ## Using Type-Safe Enums with Retries 50 | 51 | You can use the `Status` enum for more type-safe retry configuration: 52 | 53 | ```php 54 | use Fetch\Http\ClientHandler; 55 | use Fetch\Enum\Status; 56 | 57 | $response = ClientHandler::create() 58 | ->retry(3, 100) 59 | ->retryStatusCodes([ 60 | Status::TOO_MANY_REQUESTS->value, 61 | Status::SERVICE_UNAVAILABLE->value, 62 | Status::GATEWAY_TIMEOUT->value 63 | ]) 64 | ->get('https://api.example.com/unstable-endpoint'); 65 | ``` 66 | 67 | ## Customizing Retryable Status Codes 68 | 69 | By default, the client retries on these HTTP status codes: 70 | 71 | - 408 (Request Timeout) 72 | - 429 (Too Many Requests) 73 | - 500, 502, 503, 504 (Server Errors) 74 | - And several other common error codes 75 | 76 | You can customize which status codes should trigger retries: 77 | 78 | ```php 79 | $client = ClientHandler::create() 80 | ->retry(3, 100) 81 | ->retryStatusCodes([429, 503, 504]); // Only retry on these status codes 82 | 83 | $response = $client->get('https://api.example.com/unstable-endpoint'); 84 | ``` 85 | 86 | ## Customizing Retryable Exceptions 87 | 88 | By default, the client retries on network-related exceptions like `ConnectException`. You can customize which exception types should trigger retries: 89 | 90 | ```php 91 | use GuzzleHttp\Exception\ConnectException; 92 | use GuzzleHttp\Exception\RequestException; 93 | 94 | $client = ClientHandler::create() 95 | ->retry(3, 100) 96 | ->retryExceptions([ 97 | ConnectException::class, 98 | RequestException::class 99 | ]); 100 | 101 | $response = $client->get('https://api.example.com/unstable-endpoint'); 102 | ``` 103 | 104 | ## Checking Retry Configuration 105 | 106 | You can check the current retry configuration: 107 | 108 | ```php 109 | $client = ClientHandler::create()->retry(3, 200); 110 | 111 | $maxRetries = $client->getMaxRetries(); // 3 112 | $retryDelay = $client->getRetryDelay(); // 200 113 | $statusCodes = $client->getRetryableStatusCodes(); // Array of status codes 114 | $exceptions = $client->getRetryableExceptions(); // Array of exception classes 115 | ``` 116 | 117 | ## Global Retry Configuration 118 | 119 | You can set up global retry settings that apply to all requests: 120 | 121 | ```php 122 | // Configure global retry settings 123 | fetch_client([ 124 | 'retries' => 3, 125 | 'retry_delay' => 100 126 | ]); 127 | 128 | // All requests will now use these retry settings 129 | $response = fetch('https://api.example.com/users'); 130 | ``` 131 | 132 | ## Logging Retries 133 | 134 | If you've set up a logger, retry attempts will be automatically logged: 135 | 136 | ```php 137 | use Monolog\Logger; 138 | use Monolog\Handler\StreamHandler; 139 | use Fetch\Http\ClientHandler; 140 | 141 | // Create a logger 142 | $logger = new Logger('http'); 143 | $logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO)); 144 | 145 | // Create a client with logging and retries 146 | $client = ClientHandler::create(); 147 | $client->setLogger($logger); 148 | $client->retry(3, 100); 149 | 150 | // Send a request that might require retries 151 | $response = $client->get('https://api.example.com/unstable-endpoint'); 152 | 153 | // Retry attempts will be logged to logs/http.log 154 | ``` 155 | 156 | A typical retry log entry looks like: 157 | 158 | ``` 159 | [2023-09-15 14:30:12] http.INFO: Retrying request {"attempt":1,"max_attempts":3,"uri":"https://api.example.com/unstable-endpoint","method":"GET","error":"Connection timed out","code":28} 160 | ``` 161 | 162 | ## Asynchronous Retries 163 | 164 | Retries also work with asynchronous requests: 165 | 166 | ```php 167 | use function async; 168 | use function await; 169 | use function retry; 170 | 171 | // Retry asynchronous operations 172 | $result = await(retry( 173 | function() { 174 | return async(function() { 175 | return fetch('https://api.example.com/unstable-endpoint'); 176 | }); 177 | }, 178 | 3, // max attempts 179 | function($attempt) { 180 | // Exponential backoff strategy 181 | return min(pow(2, $attempt) * 100, 1000); 182 | } 183 | )); 184 | 185 | // Process the result 186 | $data = $result->json(); 187 | ``` 188 | 189 | ## Real-World Examples 190 | 191 | ### Handling Rate Limits 192 | 193 | APIs often implement rate limiting. You can configure your client to automatically retry when hitting rate limits: 194 | 195 | ```php 196 | use Fetch\Enum\Status; 197 | 198 | $client = ClientHandler::create() 199 | ->retry(3, 1000) // Longer initial delay for rate limits 200 | ->retryStatusCodes([Status::TOO_MANY_REQUESTS->value]) // Only retry on Too Many Requests 201 | ->get('https://api.example.com/rate-limited-endpoint'); 202 | ``` 203 | 204 | ### Handling Network Instability 205 | 206 | For unreliable network connections: 207 | 208 | ```php 209 | $client = ClientHandler::create() 210 | ->retry(5, 200) // More retries with moderate delay 211 | // Using default retryable status codes and exceptions 212 | ->get('https://api.example.com/endpoint'); 213 | ``` 214 | 215 | ### Handling Server Maintenance 216 | 217 | For APIs that might be temporarily down for maintenance: 218 | 219 | ```php 220 | use Fetch\Enum\Status; 221 | 222 | $client = ClientHandler::create() 223 | ->retry(10, 5000) // Many retries with long delay (5 seconds) 224 | ->retryStatusCodes([Status::SERVICE_UNAVAILABLE->value]) // Service Unavailable 225 | ->get('https://api.example.com/endpoint'); 226 | ``` 227 | 228 | ## Combining Retry with Timeout 229 | 230 | You can combine retry logic with timeout settings: 231 | 232 | ```php 233 | $client = ClientHandler::create() 234 | ->timeout(5) // 5 second timeout for each attempt 235 | ->retry(3, 100) // 3 retries with 100ms initial delay 236 | ->get('https://api.example.com/endpoint'); 237 | ``` 238 | 239 | ## Implementing Advanced Retry Logic 240 | 241 | For more complex scenarios, you can implement custom retry logic: 242 | 243 | ```php 244 | use Fetch\Http\ClientHandler; 245 | use Fetch\Http\Response; 246 | use GuzzleHttp\Exception\RequestException; 247 | use Fetch\Enum\Status; 248 | 249 | function makeRequestWithCustomRetry(string $url, int $maxAttempts = 3): Response { 250 | $attempt = 0; 251 | 252 | while (true) { 253 | try { 254 | $client = ClientHandler::create(); 255 | $response = $client->get($url); 256 | 257 | // Check if we got a success response 258 | if ($response->successful()) { 259 | return $response; 260 | } 261 | 262 | // Handle specific status codes 263 | if ($response->statusEnum() === Status::TOO_MANY_REQUESTS) { 264 | // Get retry-after header if available 265 | $retryAfter = $response->header('Retry-After'); 266 | $delay = $retryAfter ? (int) $retryAfter * 1000 : 1000; 267 | } else { 268 | // Otherwise use exponential backoff 269 | $delay = 100 * (2 ** $attempt); 270 | } 271 | 272 | // Add some jitter (±20%) 273 | $jitter = mt_rand(-20, 20) / 100; 274 | $delay = (int) ($delay * (1 + $jitter)); 275 | 276 | $attempt++; 277 | 278 | // Check if we've exceeded max attempts 279 | if ($attempt >= $maxAttempts) { 280 | return $response; // Return the last response 281 | } 282 | 283 | // Wait before retrying 284 | usleep($delay * 1000); // Convert ms to μs 285 | 286 | } catch (RequestException $e) { 287 | $attempt++; 288 | 289 | // Check if we've exceeded max attempts 290 | if ($attempt >= $maxAttempts) { 291 | throw $e; // Rethrow the last exception 292 | } 293 | 294 | // Wait before retrying 295 | $delay = 100 * (2 ** $attempt); 296 | usleep($delay * 1000); 297 | } 298 | } 299 | } 300 | 301 | // Use the custom retry function 302 | $response = makeRequestWithCustomRetry('https://api.example.com/users'); 303 | ``` 304 | 305 | ## Monitoring Retry Activity 306 | 307 | To monitor retry activity, you can combine logging with a custom callback: 308 | 309 | ```php 310 | use Monolog\Logger; 311 | use Monolog\Handler\StreamHandler; 312 | use Fetch\Http\ClientHandler; 313 | 314 | // Create a logger 315 | $logger = new Logger('retry'); 316 | $logger->pushHandler(new StreamHandler('logs/retry.log', Logger::INFO)); 317 | 318 | // Create a client with the logger 319 | $client = ClientHandler::create(); 320 | $client->setLogger($logger); 321 | $client->retry(3, 100); 322 | 323 | // Make the request 324 | $response = $client->get('https://api.example.com/unstable-endpoint'); 325 | 326 | // After the request completes, you can get debug info 327 | $debugInfo = $client->debug(); 328 | echo "Request required " . $debugInfo['retries'] . " retries\n"; 329 | ``` 330 | 331 | ## Best Practices 332 | 333 | 1. **Use Type-Safe Enums**: Leverage the Status enum for clearer and safer code when configuring retryable status codes. 334 | 335 | 2. **Start with Conservative Settings**: Begin with a small number of retries (2-3) and moderate delays (100-200ms) and adjust based on your needs. 336 | 337 | 3. **Be Mindful of Server Load**: Excessive retries can amplify problems during outages. Be respectful of the services you're calling. 338 | 339 | 4. **Use Appropriate Timeout Values**: Set reasonable timeouts in conjunction with retries to avoid long-running requests. 340 | 341 | 5. **Limit Retryable Status Codes**: Only retry on status codes that indicate transient issues. Don't retry on client errors like 400, 401, or 404. 342 | 343 | 6. **Monitor Retry Activity**: Log retry attempts to identify recurring issues with specific endpoints. 344 | 345 | 7. **Consider Retry-After Headers**: For rate limiting (429), respect the Retry-After header if provided by the server. 346 | 347 | 8. **Add Jitter**: The built-in retry mechanism includes jitter, which helps prevent "thundering herd" problems. 348 | 349 | 9. **Combine with Logging**: Always add logging when using retries to track and debug retry patterns. 350 | 351 | 10. **Use Async Retries for Parallel Operations**: When working with async code, use the retry function for better integration with the async/await pattern. 352 | 353 | ## Next Steps 354 | 355 | - Learn about [Error Handling](/guide/error-handling) for comprehensive error management 356 | - Explore [Logging](/guide/logging) for monitoring request and retry activity 357 | - See [Authentication](/guide/authentication) for handling authentication errors and retries 358 | - Check out [Asynchronous Requests](/guide/async-requests) for integrating retries with async operations 359 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | # SEO-optimized title and description 5 | title: Fetch PHP - Modern JavaScript-like HTTP Client for PHP 6 | description: A modern HTTP client library that brings JavaScript's fetch API experience to PHP with async/await patterns, promise-based API, and powerful retry mechanics. 7 | 8 | # SEO-optimized meta tags (these extend what's in config.js) 9 | head: 10 | - - meta 11 | - name: keywords 12 | content: fetch php, javascript fetch in php, async await php, http client php, promise based http, php http, guzzle alternative 13 | - - meta 14 | - property: og:title 15 | content: Fetch PHP - JavaScript's fetch API for PHP 16 | - - meta 17 | - property: og:description 18 | content: Write HTTP requests just like you would in JavaScript with PHP's Fetch API. Modern, simple HTTP client with a familiar API. 19 | - - link 20 | - rel: canonical 21 | href: https://fetch-php.thavarshan.com/ 22 | 23 | # Your existing hero section (unchanged) 24 | hero: 25 | name: Fetch PHP 26 | text: The JavaScript fetch API for PHP 27 | tagline: Modern, simple HTTP client with a familiar API 28 | image: 29 | src: /logo.png 30 | alt: Fetch PHP 31 | actions: 32 | - theme: brand 33 | text: Get Started 34 | link: /guide/quickstart 35 | - theme: alt 36 | text: View on GitHub 37 | link: https://github.com/Thavarshan/fetch-php 38 | 39 | # Your existing features section (unchanged) 40 | features: 41 | - title: Familiar API 42 | details: If you know JavaScript's fetch() API, you'll feel right at home with Fetch PHP's intuitive interface. 43 | icon: 🚀 44 | - title: Promise-Based Async 45 | details: Support for async/await-style programming with promises for concurrent HTTP requests. 46 | icon: ⚡ 47 | - title: Fluent Interface 48 | details: Chain methods together for clean, expressive code that's easy to read and maintain. 49 | icon: 🔗 50 | - title: Helper Functions 51 | details: Simple global helpers like get(), post(), and fetch() for quick and easy HTTP requests. 52 | icon: 🧰 53 | - title: PSR Compatible 54 | details: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logging) standards. 55 | icon: 🔄 56 | - title: Powerful Responses 57 | details: Rich Response objects with methods for JSON parsing, XML handling, and more. 58 | icon: 📦 59 | --- 60 | 61 | 62 | 63 | ## The Modern HTTP Client for PHP 64 | 65 | ```php 66 | // Quick API requests with fetch() 67 | $response = fetch('https://api.example.com/users'); 68 | $users = $response->json(); 69 | 70 | // Or use HTTP method helpers 71 | $user = post('https://api.example.com/users', [ 72 | 'name' => 'John Doe', 73 | 'email' => 'john@example.com' 74 | ])->json(); 75 | ``` 76 | 77 | ### Flexible Authentication 78 | 79 | ```php 80 | // Bearer token auth 81 | $response = fetch('https://api.example.com/me', [ 82 | 'token' => 'your-oauth-token' 83 | ]); 84 | 85 | // Basic auth 86 | $response = fetch('https://api.example.com/private', [ 87 | 'auth' => ['username', 'password'] 88 | ]); 89 | ``` 90 | 91 | ### Powerful Async Support 92 | 93 | ```php 94 | // Create promises for parallel requests 95 | $usersPromise = async(function() { 96 | return fetch('https://api.example.com/users'); 97 | }); 98 | 99 | $postsPromise = async(function() { 100 | return fetch('https://api.example.com/posts'); 101 | }); 102 | 103 | // Wait for all to complete 104 | all(['users' => $usersPromise, 'posts' => $postsPromise]) 105 | ->then(function ($results) { 106 | // Process results from both requests 107 | $users = $results['users']->json(); 108 | $posts = $results['posts']->json(); 109 | }); 110 | ``` 111 | 112 | ### Modern Await-Style Syntax 113 | 114 | ```php 115 | await(async(function() { 116 | // Process multiple requests in parallel 117 | $results = await(all([ 118 | 'users' => async(fn() => fetch('https://api.example.com/users')), 119 | 'posts' => async(fn() => fetch('https://api.example.com/posts')) 120 | ])); 121 | 122 | // Work with results as if they were synchronous 123 | foreach ($results['users']->json() as $user) { 124 | echo $user['name'] . "\n"; 125 | } 126 | })); 127 | ``` 128 | 129 | ## Why Fetch PHP? 130 | 131 | Fetch PHP brings the simplicity of JavaScript's fetch API to PHP, while adding powerful features like retry handling, promise-based asynchronous requests, and fluent interface for request building. It's designed to be both simple for beginners and powerful for advanced users. 132 | 133 |
134 |

Key Benefits:

135 |
    136 |
  • JavaScript-like syntax that's familiar to full-stack developers
  • 137 |
  • Promise-based API with .then(), .catch(), and .finally() methods
  • 138 |
  • Built on Guzzle for rock-solid performance with an elegant API
  • 139 |
  • Type-safe enums for HTTP methods, content types, and status codes
  • 140 |
  • Automatic retry mechanics with exponential backoff
  • 141 |
142 |
143 | 144 | ## Getting Started 145 | 146 | ```bash 147 | composer require jerome/fetch-php 148 | ``` 149 | 150 | Read the [quick start guide](/guide/quickstart) to begin working with Fetch PHP. 151 | 152 | 153 | ## Frequently Asked Questions 154 | 155 | ### How does Fetch PHP compare to Guzzle? 156 | 157 | While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing a JavaScript-like API, global client management, simplified requests, enhanced error handling, and modern PHP 8.1+ enums. 158 | 159 | ### Can I use Fetch PHP with Laravel or Symfony? 160 | 161 | Yes! Fetch PHP works seamlessly with all PHP frameworks including Laravel, Symfony, CodeIgniter, and others. It requires PHP 8.1 or higher. 162 | 163 | ### Does Fetch PHP support file uploads? 164 | 165 | Absolutely. Fetch PHP provides an elegant API for file uploads, supporting both single and multiple file uploads with progress tracking. 166 | 167 | ### Is Fetch PHP suitable for production use? 168 | 169 | Yes. Fetch PHP is built on top of Guzzle, one of the most battle-tested HTTP clients in the PHP ecosystem, while providing a more modern developer experience. 170 | 171 |
172 |

Having trouble? Open an issue on our GitHub repository.

173 |
174 | -------------------------------------------------------------------------------- /docs/map.md: -------------------------------------------------------------------------------- 1 | docs/ 2 | ├── .vitepress/ 3 | │ ├── config.js # VitePress configuration 4 | │ └── theme/ # Custom theme (if needed) 5 | ├── public/ 6 | │ ├── logo.png # Your logo 7 | │ └── favicon.ico # Favicon 8 | ├── index.md # Homepage 9 | ├── guide/ 10 | │ ├── index.md # Overview content from fetch-http-package-overview.md 11 | │ ├── installation.md # Installation instructions 12 | │ ├── quickstart.md # Quickstart guide from fetch-quickstart.md 13 | │ ├── making-requests.md # Basic request usage from fetch-http-client-usage-guide.md 14 | │ ├── helper-functions.md # Helper functions guide from helper-functions-guide.md 15 | │ ├── working-with-responses.md # From working-with-response-objects.md 16 | │ ├── request-configuration.md # From working-with-request-objects.md 17 | │ ├── authentication.md # Authentication section from usage guides 18 | │ ├── error-handling.md # Error handling patterns 19 | │ ├── logging.md # Logging configuration 20 | │ ├── async-requests.md # Async usage from fetch-http-client-usage-guide.md 21 | │ ├── promise-operations.md # Promise operations from client-handler docs 22 | │ ├── retry-handling.md # Retry configuration 23 | │ ├── file-uploads.md # File upload examples 24 | │ ├── custom-clients.md # Custom client configuration 25 | │ └── testing.md # Testing with mock responses 26 | ├── api/ 27 | │ ├── index.md # API overview 28 | │ ├── fetch.md # fetch() function reference 29 | │ ├── fetch-client.md # fetch_client() function reference 30 | │ ├── http-method-helpers.md # get(), post(), etc. helpers 31 | │ ├── client.md # Client class API reference 32 | │ ├── client-handler.md # ClientHandler API reference 33 | │ ├── request.md # Request API reference 34 | │ ├── response.md # Response API reference 35 | │ ├── method-enum.md # Method enum documentation 36 | │ ├── content-type-enum.md # ContentType enum documentation 37 | │ └── status-enum.md # Status enum documentation 38 | └── examples/ 39 | ├── index.md # Basic examples 40 | ├── api-integration.md # API integration examples 41 | ├── async-patterns.md # Advanced async examples 42 | ├── error-handling.md # Error handling examples 43 | ├── file-handling.md # File upload/download examples 44 | └── authentication.md # Authentication examples 45 | -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/fetch-php/1067988b389e1bba62812191126f33574c6c4fa3/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Sitemap: https://fetch-php.thavarshan.com/sitemap.xml 4 | -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "docs/empty-netlify/" # Point to the placeholder directory without any dependencies 3 | publish = "../.vitepress/dist" # Your actual publish directory where VitePress builds the static files 4 | command = "cd ../ && npm install && npm run build" # Run npm install and build the VitePress site 5 | 6 | [context.production.environment] 7 | NETLIFY_SKIP_COMPOSER_INSTALL = "true" 8 | NODE_VERSION = "18" # Optional: specify the Node.js version if needed 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/404.html" 13 | status = 200 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vitepress dev docs", 6 | "build": "vitepress build docs", 7 | "preview": "vitepress preview docs", 8 | "prepare": "husky" 9 | }, 10 | "devDependencies": { 11 | "@types/node": "^22.15.18", 12 | "husky": "^9.1.7", 13 | "terser": "^5.39.2", 14 | "vitepress": "latest" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/larastan/larastan/extension.neon 3 | 4 | parameters: 5 | 6 | paths: 7 | - src/Filterable 8 | 9 | level: 4 10 | 11 | excludePaths: 12 | 13 | checkMissingIterableValueType: false 14 | treatPhpDocTypesAsCertain: false 15 | -------------------------------------------------------------------------------- /phpstan.src.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 1 3 | paths: 4 | - src 5 | excludePaths: 6 | - src/Illuminate/Testing/ParallelRunner.php 7 | - src/*/views/* 8 | ignoreErrors: 9 | - "#\\(void\\) is used#" 10 | - "#Access to an undefined property#" 11 | - "#but return statement is missing.#" 12 | - "#Call to an undefined method#" 13 | - "#Caught class [a-zA-Z0-9\\\\_]+ not found.#" 14 | - "#Class [a-zA-Z0-9\\\\_]+ not found.#" 15 | - "#has invalid type#" 16 | - "#Instantiated class [a-zA-Z0-9\\\\_]+ not found.#" 17 | - "#Unsafe usage of new static#" 18 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "array_indentation": true, 3 | "array_syntax": { 4 | "syntax": "short" 5 | }, 6 | "binary_operator_spaces": { 7 | "default": "single_space" 8 | }, 9 | "blank_line_after_namespace": true, 10 | "blank_line_after_opening_tag": true, 11 | "blank_line_before_statement": { 12 | "statements": [ 13 | "continue", 14 | "return" 15 | ] 16 | }, 17 | "blank_line_between_import_groups": true, 18 | "blank_lines_before_namespace": true, 19 | "braces_position": { 20 | "control_structures_opening_brace": "same_line", 21 | "functions_opening_brace": "next_line_unless_newline_at_signature_end", 22 | "anonymous_functions_opening_brace": "same_line", 23 | "classes_opening_brace": "next_line_unless_newline_at_signature_end", 24 | "anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end", 25 | "allow_single_line_empty_anonymous_classes": false, 26 | "allow_single_line_anonymous_functions": false 27 | }, 28 | "cast_spaces": true, 29 | "class_attributes_separation": { 30 | "elements": { 31 | "const": "one", 32 | "method": "one", 33 | "property": "one", 34 | "trait_import": "none" 35 | } 36 | }, 37 | "class_definition": { 38 | "multi_line_extends_each_single_line": true, 39 | "single_item_single_line": true, 40 | "single_line": true 41 | }, 42 | "clean_namespace": true, 43 | "compact_nullable_type_declaration": true, 44 | "concat_space": { 45 | "spacing": "none" 46 | }, 47 | "constant_case": { 48 | "case": "lower" 49 | }, 50 | "control_structure_braces": true, 51 | "control_structure_continuation_position": { 52 | "position": "same_line" 53 | }, 54 | "declare_equal_normalize": true, 55 | "declare_parentheses": true, 56 | "elseif": true, 57 | "encoding": true, 58 | "full_opening_tag": true, 59 | "fully_qualified_strict_types": false, 60 | "function_declaration": true, 61 | "general_phpdoc_tag_rename": true, 62 | "heredoc_to_nowdoc": true, 63 | "include": true, 64 | "increment_style": { 65 | "style": "post" 66 | }, 67 | "indentation_type": true, 68 | "integer_literal_case": true, 69 | "lambda_not_used_import": true, 70 | "line_ending": true, 71 | "linebreak_after_opening_tag": true, 72 | "list_syntax": true, 73 | "lowercase_cast": true, 74 | "lowercase_keywords": true, 75 | "lowercase_static_reference": true, 76 | "magic_constant_casing": true, 77 | "magic_method_casing": true, 78 | "method_chaining_indentation": true, 79 | "multiline_whitespace_before_semicolons": { 80 | "strategy": "no_multi_line" 81 | }, 82 | "native_function_casing": true, 83 | "native_type_declaration_casing": true, 84 | "new_with_parentheses": { 85 | "named_class": false, 86 | "anonymous_class": false 87 | }, 88 | "no_alias_functions": true, 89 | "no_alias_language_construct_call": true, 90 | "no_alternative_syntax": true, 91 | "no_binary_string": true, 92 | "no_blank_lines_after_class_opening": true, 93 | "no_blank_lines_after_phpdoc": true, 94 | "no_closing_tag": true, 95 | "no_empty_phpdoc": true, 96 | "no_empty_statement": true, 97 | "no_extra_blank_lines": { 98 | "tokens": [ 99 | "extra", 100 | "throw", 101 | "use" 102 | ] 103 | }, 104 | "no_leading_import_slash": true, 105 | "no_leading_namespace_whitespace": true, 106 | "no_mixed_echo_print": { 107 | "use": "echo" 108 | }, 109 | "no_multiline_whitespace_around_double_arrow": true, 110 | "no_multiple_statements_per_line": true, 111 | "no_short_bool_cast": true, 112 | "no_singleline_whitespace_before_semicolons": true, 113 | "no_space_around_double_colon": true, 114 | "no_spaces_after_function_name": true, 115 | "no_spaces_around_offset": { 116 | "positions": [ 117 | "inside", 118 | "outside" 119 | ] 120 | }, 121 | "no_superfluous_phpdoc_tags": { 122 | "allow_mixed": true, 123 | "allow_unused_params": true 124 | }, 125 | "no_trailing_comma_in_singleline": true, 126 | "no_trailing_whitespace": true, 127 | "no_trailing_whitespace_in_comment": true, 128 | "no_unneeded_control_parentheses": { 129 | "statements": [ 130 | "break", 131 | "clone", 132 | "continue", 133 | "echo_print", 134 | "return", 135 | "switch_case", 136 | "yield" 137 | ] 138 | }, 139 | "no_unneeded_braces": true, 140 | "no_unreachable_default_argument_value": true, 141 | "no_unset_cast": true, 142 | "no_unused_imports": true, 143 | "no_useless_return": true, 144 | "no_whitespace_before_comma_in_array": true, 145 | "no_whitespace_in_blank_line": true, 146 | "normalize_index_brace": true, 147 | "not_operator_with_successor_space": true, 148 | "nullable_type_declaration": true, 149 | "nullable_type_declaration_for_default_null_value": true, 150 | "object_operator_without_whitespace": true, 151 | "ordered_imports": { 152 | "sort_algorithm": "alpha", 153 | "imports_order": [ 154 | "const", 155 | "class", 156 | "function" 157 | ] 158 | }, 159 | "ordered_interfaces": true, 160 | "ordered_traits": true, 161 | "php_unit_method_casing": { 162 | "case": "snake_case" 163 | }, 164 | "phpdoc_align": { 165 | "align": "left", 166 | "spacing": { 167 | "param": 2 168 | } 169 | }, 170 | "phpdoc_indent": true, 171 | "phpdoc_inline_tag_normalizer": true, 172 | "phpdoc_no_access": true, 173 | "phpdoc_no_package": true, 174 | "phpdoc_no_useless_inheritdoc": true, 175 | "phpdoc_order": { 176 | "order": [ 177 | "param", 178 | "return", 179 | "throws" 180 | ] 181 | }, 182 | "phpdoc_scalar": true, 183 | "phpdoc_separation": { 184 | "groups": [ 185 | [ 186 | "deprecated", 187 | "link", 188 | "see", 189 | "since" 190 | ], 191 | [ 192 | "author", 193 | "copyright", 194 | "license" 195 | ], 196 | [ 197 | "category", 198 | "package", 199 | "subpackage" 200 | ], 201 | [ 202 | "property", 203 | "property-read", 204 | "property-write" 205 | ], 206 | [ 207 | "param", 208 | "return" 209 | ] 210 | ] 211 | }, 212 | "phpdoc_single_line_var_spacing": true, 213 | "phpdoc_summary": false, 214 | "phpdoc_tag_type": { 215 | "tags": { 216 | "inheritdoc": "inline" 217 | } 218 | }, 219 | "phpdoc_to_comment": false, 220 | "phpdoc_trim": true, 221 | "phpdoc_types": true, 222 | "phpdoc_var_without_name": true, 223 | "psr_autoloading": false, 224 | "return_type_declaration": { 225 | "space_before": "none" 226 | }, 227 | "self_accessor": false, 228 | "self_static_accessor": true, 229 | "short_scalar_cast": true, 230 | "simplified_null_return": false, 231 | "single_blank_line_at_eof": true, 232 | "single_class_element_per_statement": { 233 | "elements": [ 234 | "const", 235 | "property" 236 | ] 237 | }, 238 | "single_import_per_statement": { 239 | "group_to_single_imports": false 240 | }, 241 | "single_line_after_imports": true, 242 | "single_line_comment_style": { 243 | "comment_types": [ 244 | "hash" 245 | ] 246 | }, 247 | "single_line_empty_body": true, 248 | "single_quote": true, 249 | "single_space_around_construct": true, 250 | "space_after_semicolon": true, 251 | "spaces_inside_parentheses": true, 252 | "standardize_not_equals": true, 253 | "statement_indentation": true, 254 | "switch_case_semicolon_to_colon": true, 255 | "switch_case_space": true, 256 | "ternary_operator_spaces": true, 257 | "trailing_comma_in_multiline": { 258 | "elements": [ 259 | "arrays" 260 | ] 261 | }, 262 | "trim_array_spaces": true, 263 | "type_declaration_spaces": true, 264 | "types_spaces": true, 265 | "unary_operator_spaces": true, 266 | "visibility_required": { 267 | "elements": [ 268 | "method", 269 | "property" 270 | ] 271 | }, 272 | "single_trait_insert_per_statement": false, 273 | "whitespace_after_comma_in_array": true, 274 | "yoda_style": { 275 | "always_move_variable": false, 276 | "equal": false, 277 | "identical": false, 278 | "less_and_greater": false 279 | }, 280 | "method_argument_space": { 281 | "on_multiline": "ensure_fully_multiline", 282 | "after_heredoc": true 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/Fetch/Concerns/HandlesUris.php: -------------------------------------------------------------------------------- 1 | options['base_uri'] ?? ''; 23 | $queryParams = $this->options['query'] ?? []; 24 | 25 | // Normalize URIs before processing 26 | $uri = $this->normalizeUri($uri); 27 | if (! empty($baseUri)) { 28 | $baseUri = $this->normalizeUri($baseUri); 29 | } 30 | 31 | // Validate inputs 32 | $this->validateUriInputs($uri, $baseUri); 33 | 34 | // Build the final URI 35 | $fullUri = $this->isAbsoluteUrl($uri) 36 | ? $uri 37 | : $this->joinUriPaths($baseUri, $uri); 38 | 39 | // Add query parameters if any 40 | return $this->appendQueryParameters($fullUri, $queryParams); 41 | } 42 | 43 | /** 44 | * Get the full URI using the URI from options. 45 | * 46 | * @return string The full URI 47 | * 48 | * @throws InvalidArgumentException If the URI is invalid 49 | */ 50 | protected function getFullUri(): string 51 | { 52 | $uri = $this->options['uri'] ?? ''; 53 | 54 | return $this->buildFullUri($uri); 55 | } 56 | 57 | /** 58 | * Validate URI and base URI inputs. 59 | * 60 | * @param string $uri The URI or path 61 | * @param string $baseUri The base URI 62 | * 63 | * @throws InvalidArgumentException If validation fails 64 | */ 65 | protected function validateUriInputs(string $uri, string $baseUri): void 66 | { 67 | // Check if we have any URI to work with 68 | if (empty($uri) && empty($baseUri)) { 69 | throw new InvalidArgumentException('URI cannot be empty'); 70 | } 71 | 72 | // For relative URIs, ensure we have a base URI 73 | if (! $this->isAbsoluteUrl($uri) && empty($baseUri)) { 74 | throw new InvalidArgumentException( 75 | "Relative URI '{$uri}' cannot be used without a base URI. ". 76 | 'Set a base URI using the baseUri() method.' 77 | ); 78 | } 79 | 80 | // Ensure base URI is valid if provided 81 | if (! empty($baseUri) && ! $this->isAbsoluteUrl($baseUri)) { 82 | throw new InvalidArgumentException("Invalid base URI: {$baseUri}"); 83 | } 84 | } 85 | 86 | /** 87 | * Check if a URI is an absolute URL. 88 | * 89 | * @param string $uri The URI to check 90 | * @return bool Whether the URI is absolute 91 | */ 92 | protected function isAbsoluteUrl(string $uri): bool 93 | { 94 | return filter_var($uri, \FILTER_VALIDATE_URL) !== false; 95 | } 96 | 97 | /** 98 | * Join base URI with a path properly. 99 | * 100 | * @param string $baseUri The base URI 101 | * @param string $path The path to append 102 | * @return string The combined URI 103 | */ 104 | protected function joinUriPaths(string $baseUri, string $path): string 105 | { 106 | return rtrim($baseUri, '/').'/'.ltrim($path, '/'); 107 | } 108 | 109 | /** 110 | * Append query parameters to a URI. 111 | * 112 | * @param string $uri The URI 113 | * @param array $queryParams The query parameters 114 | * @return string The URI with query parameters 115 | */ 116 | protected function appendQueryParameters(string $uri, array $queryParams): string 117 | { 118 | if (empty($queryParams)) { 119 | return $uri; 120 | } 121 | 122 | // Split URI to preserve any fragment 123 | [$baseUri, $fragment] = $this->splitUriFragment($uri); 124 | 125 | // Determine the separator based on URI structure 126 | $separator = $this->getQuerySeparator($baseUri); 127 | 128 | // Build the query string 129 | $queryString = http_build_query($queryParams); 130 | 131 | // Combine everything 132 | return $baseUri.$separator.$queryString.$fragment; 133 | } 134 | 135 | /** 136 | * Split a URI into its base and fragment parts. 137 | * 138 | * @param string $uri The URI to split 139 | * @return array{0: string, 1: string} [baseUri, fragment] 140 | */ 141 | protected function splitUriFragment(string $uri): array 142 | { 143 | $fragments = explode('#', $uri, 2); 144 | $baseUri = $fragments[0]; 145 | $fragment = isset($fragments[1]) ? '#'.$fragments[1] : ''; 146 | 147 | return [$baseUri, $fragment]; 148 | } 149 | 150 | /** 151 | * Determine the appropriate query string separator for a URI. 152 | * 153 | * @param string $uri The URI 154 | * @return string The separator ('?' or '&' or '') 155 | */ 156 | protected function getQuerySeparator(string $uri): string 157 | { 158 | // Handle special case: URI already ends with a question mark 159 | if (str_ends_with($uri, '?')) { 160 | return ''; 161 | } 162 | 163 | // Check if the URI already has query parameters 164 | $parsedUrl = parse_url($uri); 165 | 166 | return isset($parsedUrl['query']) && ! empty($parsedUrl['query']) ? '&' : '?'; 167 | } 168 | 169 | /** 170 | * Normalize a URI by removing redundant slashes. 171 | * 172 | * @param string $uri The URI to normalize 173 | * @return string The normalized URI 174 | */ 175 | protected function normalizeUri(string $uri): string 176 | { 177 | // Extract scheme if present (e.g., http://) 178 | if (preg_match('~^(https?://)~i', $uri, $matches)) { 179 | $scheme = $matches[1]; 180 | $rest = substr($uri, strlen($scheme)); 181 | // Normalize consecutive slashes in the path 182 | $rest = preg_replace('~//+~', '/', $rest); 183 | 184 | return $scheme.$rest; 185 | } 186 | 187 | // For non-URLs, just normalize consecutive slashes 188 | return preg_replace('~//+~', '/', $uri); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Fetch/Concerns/ManagesRetries.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected array $retryableStatusCodes = [ 33 | 408, 429, 500, 502, 503, 34 | 504, 507, 509, 520, 521, 35 | 522, 523, 525, 527, 530, 36 | ]; 37 | 38 | /** 39 | * The exceptions that should be retried. 40 | * 41 | * @var array> 42 | */ 43 | protected array $retryableExceptions = [ 44 | ConnectException::class, 45 | ]; 46 | 47 | /** 48 | * Set the retry logic for the request. 49 | * 50 | * @param int $retries Maximum number of retry attempts 51 | * @param int $delay Initial delay in milliseconds 52 | * @return $this 53 | * 54 | * @throws InvalidArgumentException If the parameters are invalid 55 | */ 56 | public function retry(int $retries, int $delay = 100): ClientHandler 57 | { 58 | if ($retries < 0) { 59 | throw new InvalidArgumentException('Retries must be a non-negative integer'); 60 | } 61 | 62 | if ($delay < 0) { 63 | throw new InvalidArgumentException('Delay must be a non-negative integer'); 64 | } 65 | 66 | $this->maxRetries = $retries; 67 | $this->retryDelay = $delay; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Set the status codes that should be retried. 74 | * 75 | * @param array $statusCodes HTTP status codes 76 | * @return $this 77 | */ 78 | public function retryStatusCodes(array $statusCodes): ClientHandler 79 | { 80 | $this->retryableStatusCodes = array_map('intval', $statusCodes); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Set the exception types that should be retried. 87 | * 88 | * @param array> $exceptions Exception class names 89 | * @return $this 90 | */ 91 | public function retryExceptions(array $exceptions): ClientHandler 92 | { 93 | $this->retryableExceptions = $exceptions; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Get the current maximum retries setting. 100 | * 101 | * @return int The maximum retries 102 | */ 103 | public function getMaxRetries(): int 104 | { 105 | return $this->maxRetries ?? self::DEFAULT_RETRIES; 106 | } 107 | 108 | /** 109 | * Get the current retry delay setting. 110 | * 111 | * @return int The retry delay in milliseconds 112 | */ 113 | public function getRetryDelay(): int 114 | { 115 | return $this->retryDelay ?? self::DEFAULT_RETRY_DELAY; 116 | } 117 | 118 | /** 119 | * Get the retryable status codes. 120 | * 121 | * @return array The retryable HTTP status codes 122 | */ 123 | public function getRetryableStatusCodes(): array 124 | { 125 | return $this->retryableStatusCodes; 126 | } 127 | 128 | /** 129 | * Get the retryable exception types. 130 | * 131 | * @return array> The retryable exception classes 132 | */ 133 | public function getRetryableExceptions(): array 134 | { 135 | return $this->retryableExceptions; 136 | } 137 | 138 | /** 139 | * Implement retry logic for the request with exponential backoff. 140 | * 141 | * @param callable $request The request to execute 142 | * @return ResponseInterface The response after successful execution 143 | * 144 | * @throws RequestException If the request fails after all retries 145 | * @throws RuntimeException If something unexpected happens 146 | */ 147 | protected function retryRequest(callable $request): ResponseInterface 148 | { 149 | $attempts = $this->maxRetries ?? self::DEFAULT_RETRIES; 150 | $delay = $this->retryDelay ?? self::DEFAULT_RETRY_DELAY; 151 | $exceptions = []; 152 | 153 | for ($attempt = 0; $attempt <= $attempts; $attempt++) { 154 | try { 155 | // Execute the request 156 | return $request(); 157 | } catch (RequestException $e) { 158 | // Collect exception for later 159 | $exceptions[] = $e; 160 | 161 | // If this was the last attempt, break to throw the most recent exception 162 | if ($attempt === $attempts) { 163 | break; 164 | } 165 | 166 | // Only retry on retryable errors 167 | if (! $this->isRetryableError($e)) { 168 | throw $e; 169 | } 170 | 171 | // Log the retry for debugging purposes 172 | if (method_exists($this, 'logRetry')) { 173 | $this->logRetry($attempt + 1, $attempts, $e); 174 | } 175 | 176 | // Calculate delay with exponential backoff and jitter 177 | $currentDelay = $this->calculateBackoffDelay($delay, $attempt); 178 | 179 | // Sleep before the next retry 180 | usleep($currentDelay * 1000); // Convert milliseconds to microseconds 181 | } catch (Throwable $e) { 182 | // Handle unexpected exceptions (not RequestException) 183 | throw new RuntimeException( 184 | sprintf('Unexpected error during request: %s', $e->getMessage()), 185 | (int) $e->getCode(), 186 | $e 187 | ); 188 | } 189 | } 190 | 191 | // If we got here, all retries failed 192 | $lastException = end($exceptions) ?: new RuntimeException('Request failed after all retries'); 193 | 194 | // Enhanced failure reporting 195 | if ($lastException instanceof RequestException) { 196 | $statusCode = $lastException->getCode(); 197 | throw new RuntimeException( 198 | sprintf( 199 | 'Request failed after %d attempts with status code %d: %s', 200 | $attempts + 1, 201 | $statusCode, 202 | $lastException->getMessage() 203 | ), 204 | $statusCode, 205 | $lastException 206 | ); 207 | } 208 | 209 | throw $lastException; 210 | } 211 | 212 | /** 213 | * Calculate backoff delay with exponential growth and jitter. 214 | * 215 | * @param int $baseDelay The base delay in milliseconds 216 | * @param int $attempt The current attempt number (0-based) 217 | * @return int The calculated delay in milliseconds 218 | */ 219 | protected function calculateBackoffDelay(int $baseDelay, int $attempt): int 220 | { 221 | // Exponential backoff: baseDelay * 2^attempt 222 | $exponentialDelay = $baseDelay * (2 ** $attempt); 223 | 224 | // Add jitter: random value between 0-100% of the calculated delay 225 | $jitter = mt_rand(0, 100) / 100; // Random value between 0 and 1 226 | $delay = (int) ($exponentialDelay * (1 + $jitter)); 227 | 228 | // Cap the maximum delay at 30 seconds (30000ms) 229 | return min($delay, 30000); 230 | } 231 | 232 | /** 233 | * Determine if an error is retryable. 234 | * 235 | * @param RequestException $e The exception to check 236 | * @return bool Whether the error is retryable 237 | */ 238 | protected function isRetryableError(RequestException $e): bool 239 | { 240 | $statusCode = $e->getCode(); 241 | 242 | // Check if the status code is in our list of retryable codes 243 | $isRetryableStatusCode = in_array($statusCode, $this->retryableStatusCodes, true); 244 | 245 | // Check if the exception or its previous is one of our retryable exception types 246 | $isRetryableException = false; 247 | $exception = $e; 248 | 249 | while ($exception) { 250 | if (in_array(get_class($exception), $this->retryableExceptions, true)) { 251 | $isRetryableException = true; 252 | break; 253 | } 254 | $exception = $exception->getPrevious(); 255 | } 256 | 257 | return $isRetryableStatusCode || $isRetryableException; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Fetch/Enum/ContentType.php: -------------------------------------------------------------------------------- 1 | true, 89 | // These are binary/non-text content types 90 | self::MULTIPART => false, 91 | // Default for any new enum values added in the future 92 | default => false, 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Fetch/Enum/Method.php: -------------------------------------------------------------------------------- 1 | 'Continue', 106 | self::SWITCHING_PROTOCOLS => 'Switching Protocols', 107 | self::PROCESSING => 'Processing', 108 | self::EARLY_HINTS => 'Early Hints', 109 | self::OK => 'OK', 110 | self::CREATED => 'Created', 111 | self::ACCEPTED => 'Accepted', 112 | self::NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information', 113 | self::NO_CONTENT => 'No Content', 114 | self::RESET_CONTENT => 'Reset Content', 115 | self::PARTIAL_CONTENT => 'Partial Content', 116 | self::MULTI_STATUS => 'Multi-Status', 117 | self::ALREADY_REPORTED => 'Already Reported', 118 | self::IM_USED => 'IM Used', 119 | self::MULTIPLE_CHOICES => 'Multiple Choices', 120 | self::MOVED_PERMANENTLY => 'Moved Permanently', 121 | self::FOUND => 'Found', 122 | self::SEE_OTHER => 'See Other', 123 | self::NOT_MODIFIED => 'Not Modified', 124 | self::USE_PROXY => 'Use Proxy', 125 | self::TEMPORARY_REDIRECT => 'Temporary Redirect', 126 | self::PERMANENT_REDIRECT => 'Permanent Redirect', 127 | self::BAD_REQUEST => 'Bad Request', 128 | self::UNAUTHORIZED => 'Unauthorized', 129 | self::PAYMENT_REQUIRED => 'Payment Required', 130 | self::FORBIDDEN => 'Forbidden', 131 | self::NOT_FOUND => 'Not Found', 132 | self::METHOD_NOT_ALLOWED => 'Method Not Allowed', 133 | self::NOT_ACCEPTABLE => 'Not Acceptable', 134 | self::PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', 135 | self::REQUEST_TIMEOUT => 'Request Timeout', 136 | self::CONFLICT => 'Conflict', 137 | self::GONE => 'Gone', 138 | self::LENGTH_REQUIRED => 'Length Required', 139 | self::PRECONDITION_FAILED => 'Precondition Failed', 140 | self::PAYLOAD_TOO_LARGE => 'Payload Too Large', 141 | self::URI_TOO_LONG => 'URI Too Long', 142 | self::UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', 143 | self::RANGE_NOT_SATISFIABLE => 'Range Not Satisfiable', 144 | self::EXPECTATION_FAILED => 'Expectation Failed', 145 | self::IM_A_TEAPOT => 'I\'m a teapot', 146 | self::MISDIRECTED_REQUEST => 'Misdirected Request', 147 | self::UNPROCESSABLE_ENTITY => 'Unprocessable Entity', 148 | self::LOCKED => 'Locked', 149 | self::FAILED_DEPENDENCY => 'Failed Dependency', 150 | self::TOO_EARLY => 'Too Early', 151 | self::UPGRADE_REQUIRED => 'Upgrade Required', 152 | self::PRECONDITION_REQUIRED => 'Precondition Required', 153 | self::TOO_MANY_REQUESTS => 'Too Many Requests', 154 | self::REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', 155 | self::UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', 156 | self::INTERNAL_SERVER_ERROR => 'Internal Server Error', 157 | self::NOT_IMPLEMENTED => 'Not Implemented', 158 | self::BAD_GATEWAY => 'Bad Gateway', 159 | self::SERVICE_UNAVAILABLE => 'Service Unavailable', 160 | self::GATEWAY_TIMEOUT => 'Gateway Timeout', 161 | self::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported', 162 | self::VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', 163 | self::INSUFFICIENT_STORAGE => 'Insufficient Storage', 164 | self::LOOP_DETECTED => 'Loop Detected', 165 | self::NOT_EXTENDED => 'Not Extended', 166 | self::NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', 167 | }; 168 | } 169 | 170 | /** 171 | * Check if the status code is informational (1xx). 172 | */ 173 | public function isInformational(): bool 174 | { 175 | return $this->value >= 100 && $this->value < 200; 176 | } 177 | 178 | /** 179 | * Check if the status code indicates success (2xx). 180 | */ 181 | public function isSuccess(): bool 182 | { 183 | return $this->value >= 200 && $this->value < 300; 184 | } 185 | 186 | /** 187 | * Check if the status code indicates redirection (3xx). 188 | */ 189 | public function isRedirection(): bool 190 | { 191 | return $this->value >= 300 && $this->value < 400; 192 | } 193 | 194 | /** 195 | * Check if the status code indicates client error (4xx). 196 | */ 197 | public function isClientError(): bool 198 | { 199 | return $this->value >= 400 && $this->value < 500; 200 | } 201 | 202 | /** 203 | * Check if the status code indicates server error (5xx). 204 | */ 205 | public function isServerError(): bool 206 | { 207 | return $this->value >= 500 && $this->value < 600; 208 | } 209 | 210 | /** 211 | * Check if the status code indicates an error (4xx or 5xx). 212 | */ 213 | public function isError(): bool 214 | { 215 | return $this->isClientError() || $this->isServerError(); 216 | } 217 | 218 | /** 219 | * Check if the response is cacheable according to HTTP specifications. 220 | */ 221 | public function isCacheable(): bool 222 | { 223 | return match ($this) { 224 | self::OK, self::NON_AUTHORITATIVE_INFORMATION, self::PARTIAL_CONTENT, 225 | self::MULTIPLE_CHOICES, self::MOVED_PERMANENTLY, self::NOT_FOUND, 226 | self::METHOD_NOT_ALLOWED, self::GONE => true, 227 | default => false, 228 | }; 229 | } 230 | 231 | /** 232 | * Check if the status code indicates that the resource was not modified. 233 | */ 234 | public function isNotModified(): bool 235 | { 236 | return $this === self::NOT_MODIFIED; 237 | } 238 | 239 | /** 240 | * Check if the status code indicates that the resource was empty (204, 304). 241 | */ 242 | public function isEmpty(): bool 243 | { 244 | return $this === self::NO_CONTENT || $this === self::NOT_MODIFIED; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Fetch/Exceptions/ClientException.php: -------------------------------------------------------------------------------- 1 | response; 20 | } 21 | 22 | /** 23 | * Set the HTTP response that caused the exception. 24 | */ 25 | public function setResponse(ResponseInterface $response): self 26 | { 27 | $this->response = $response; 28 | 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Fetch/Exceptions/NetworkException.php: -------------------------------------------------------------------------------- 1 | request = $request; 29 | } 30 | 31 | /** 32 | * Get the request. 33 | * 34 | * @return RequestInterface The request 35 | */ 36 | public function getRequest(): RequestInterface 37 | { 38 | return $this->request; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Fetch/Exceptions/RequestException.php: -------------------------------------------------------------------------------- 1 | request = $request; 40 | $this->response = $response; 41 | } 42 | 43 | /** 44 | * Get the request. 45 | * 46 | * @return RequestInterface The request 47 | */ 48 | public function getRequest(): RequestInterface 49 | { 50 | return $this->request; 51 | } 52 | 53 | /** 54 | * Get the response if available. 55 | * 56 | * @return ResponseInterface|null The response or null 57 | */ 58 | public function getResponse(): ?ResponseInterface 59 | { 60 | return $this->response; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Fetch/Interfaces/Response.php: -------------------------------------------------------------------------------- 1 | toStatic($new); 20 | } 21 | 22 | /** 23 | * Return an instance without the specified header. 24 | */ 25 | public function withoutHeader($name): static 26 | { 27 | $new = parent::withoutHeader($name); 28 | 29 | return $this->toStatic($new); 30 | } 31 | 32 | /** 33 | * Return an instance with the provided value replacing the specified header. 34 | */ 35 | public function withHeader($name, $value): static 36 | { 37 | $new = parent::withHeader($name, $value); 38 | 39 | return $this->toStatic($new); 40 | } 41 | 42 | /** 43 | * Return an instance with the specified protocol version. 44 | */ 45 | public function withProtocolVersion($version): static 46 | { 47 | $new = parent::withProtocolVersion($version); 48 | 49 | return $this->toStatic($new); 50 | } 51 | 52 | /** 53 | * Return an instance with the specified URI. 54 | */ 55 | public function withUri(UriInterface $uri, $preserveHost = false): static 56 | { 57 | $new = parent::withUri($uri, $preserveHost); 58 | 59 | return $this->toStatic($new); 60 | } 61 | 62 | /** 63 | * Return an instance with the provided HTTP method. 64 | */ 65 | public function withMethod($method): static 66 | { 67 | $new = parent::withMethod($method); 68 | 69 | return $this->toStatic($new); 70 | } 71 | 72 | /** 73 | * Convert a parent method result to the current class type. 74 | * This preserves all properties including custom request target. 75 | */ 76 | protected function toStatic(RequestInterface $new): static 77 | { 78 | // Get the custom request target if this class has it set 79 | $requestTarget = null; 80 | if (property_exists($this, 'customRequestTarget') && $this->customRequestTarget !== null) { 81 | $requestTarget = $this->customRequestTarget; 82 | } 83 | 84 | // If the new instance has a different request target than what's derived from its URI, 85 | // it means it has a custom request target set 86 | $defaultTarget = '/'.ltrim($new->getUri()->getPath(), '/'); 87 | $query = $new->getUri()->getQuery(); 88 | if ($query !== '') { 89 | $defaultTarget .= '?'.$query; 90 | } 91 | 92 | if ($new->getRequestTarget() !== $defaultTarget) { 93 | $requestTarget = $new->getRequestTarget(); 94 | } 95 | 96 | // Create new instance with all properties 97 | $instance = new static( 98 | $new->getMethod(), 99 | $new->getUri(), 100 | $new->getHeaders(), 101 | $new->getBody(), 102 | $new->getProtocolVersion(), 103 | $requestTarget 104 | ); 105 | 106 | return $instance; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Fetch/Traits/ResponseImmutabilityTrait.php: -------------------------------------------------------------------------------- 1 | toStatic(parent::withStatus($code, $reasonPhrase)); 18 | } 19 | 20 | /** 21 | * Return an instance with the specified header appended with the given value. 22 | */ 23 | public function withAddedHeader($name, $value): static 24 | { 25 | return $this->toStatic(parent::withAddedHeader($name, $value)); 26 | } 27 | 28 | /** 29 | * Return an instance without the specified header. 30 | */ 31 | public function withoutHeader($name): static 32 | { 33 | return $this->toStatic(parent::withoutHeader($name)); 34 | } 35 | 36 | /** 37 | * Return an instance with the provided value replacing the specified header. 38 | */ 39 | public function withHeader($name, $value): static 40 | { 41 | return $this->toStatic(parent::withHeader($name, $value)); 42 | } 43 | 44 | /** 45 | * Return an instance with the specified protocol version. 46 | */ 47 | public function withProtocolVersion($version): static 48 | { 49 | return $this->toStatic(parent::withProtocolVersion($version)); 50 | } 51 | 52 | /** 53 | * Return an instance with the specified body. 54 | */ 55 | public function withBody(StreamInterface $body): static 56 | { 57 | $new = parent::withBody($body); 58 | $response = $this->toStatic($new); 59 | 60 | // Update the buffered body contents 61 | if (property_exists($this, 'bodyContents')) { 62 | $response->bodyContents = (string) $body; 63 | } 64 | 65 | return $response; 66 | } 67 | 68 | /** 69 | * Convert a parent method result to the current class type. 70 | */ 71 | protected function toStatic(ResponseInterface $new): static 72 | { 73 | return new static( 74 | $new->getStatusCode(), 75 | $new->getHeaders(), 76 | (string) $new->getBody(), 77 | $new->getProtocolVersion(), 78 | $new->getReasonPhrase() 79 | ); 80 | } 81 | } 82 | --------------------------------------------------------------------------------