[
  {
    "path": ".gitattributes",
    "content": "# Ignore all test and documentation with \"export-ignore\".\n/.github           export-ignore\n/docs              export-ignore\n/tests             export-ignore\n/.gitattributes    export-ignore\n/.gitignore        export-ignore\n/.styleci.yml      export-ignore\n/phpstan.neon      export-ignore\n/phpunit.xml       export-ignore\n/CHANGELOG.md      export-ignore\n/UPGRADE.md        export-ignore\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: ash-jc-allen\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: composer\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/ci-phpstan.yml",
    "content": "name: run-phpstan\n\non:\n  pull_request:\n\njobs:\n  run-tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        php: [8.1, 8.2, 8.3, 8.4, 8.5]\n        laravel: [9.*, 10.*, 11.*, 12.*, 13.*]\n        include:\n          - laravel: 13.*\n            testbench: 11.*\n          - laravel: 12.*\n            testbench: 10.*\n          - laravel: 11.*\n            testbench: 9.*\n          - laravel: 10.*\n            testbench: 8.*\n          - laravel: 9.*\n            testbench: 7.*\n        exclude:\n          - php: 8.1\n            laravel: 11.*\n          - php: 8.1\n            laravel: 12.*\n          - php: 8.1\n            laravel: 13.*\n          - php: 8.2\n            laravel: 13.*\n\n    name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}\n\n    steps:\n      - name: Update apt\n        run: sudo apt-get update --fix-missing\n\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          coverage: none\n\n      - name: Setup Problem Matches\n        run: |\n          echo \"::add-matcher::${{ runner.tool_cache }}/php.json\"\n          echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\"\n\n      - name: Install dependencies\n        run: |\n          composer require \"laravel/framework:${{ matrix.laravel }}\" \"orchestra/testbench:${{ matrix.testbench }}\" --no-interaction --no-update\n          composer update --prefer-dist --no-interaction --no-suggest\n      - name: Run Larastan\n        run: composer larastan\n"
  },
  {
    "path": ".github/workflows/ci-tests.yml",
    "content": "name: run-tests\n\non:\n  pull_request:\n\njobs:\n  run-tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        php: [8.1, 8.2, 8.3, 8.4, 8.5]\n        laravel: [9.*, 10.*, 11.*, 12.*, 13.*]\n        include:\n          - laravel: 13.*\n            testbench: 11.*\n          - laravel: 12.*\n            testbench: 10.*\n          - laravel: 11.*\n            testbench: 9.*\n          - laravel: 10.*\n            testbench: 8.*\n          - laravel: 9.*\n            testbench: 7.*\n        exclude:\n          - php: 8.1\n            laravel: 11.*\n          - php: 8.1\n            laravel: 12.*\n          - php: 8.1\n            laravel: 13.*\n          - php: 8.2\n            laravel: 13.*\n\n    name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}\n\n    steps:\n      - name: Update apt\n        run: sudo apt-get update --fix-missing\n\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          coverage: none\n\n      - name: Setup Problem Matches\n        run: |\n          echo \"::add-matcher::${{ runner.tool_cache }}/php.json\"\n          echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\"\n\n      - name: Install dependencies\n        run: |\n          composer require \"laravel/framework:${{ matrix.laravel }}\" \"orchestra/testbench:${{ matrix.testbench }}\" --no-interaction --no-update\n          composer update --prefer-dist --no-interaction --no-suggest\n      - name: Execute tests\n        run: composer test\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\nvendor/\ncomposer.lock\n.phpunit.result.cache"
  },
  {
    "path": ".styleci.yml",
    "content": "preset: laravel"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n**v3.11.0 (released 2026-03-13):**\n\n- Added support for Laravel 13. ((#96)[https://github.com/ash-jc-allen/favicon-fetcher/pull/96])\n- Run CI workflows with PHP 8.5. ([#95](https://github.com/ash-jc-allen/favicon-fetcher/pull/95))\n\n**v3.10.0 (released 2025-10-17):**\n\n- Fixed typo. ([#93](https://github.com/ash-jc-allen/favicon-fetcher/pull/93))\n- Added `.gitattributes` file. ([#94](https://github.com/ash-jc-allen/favicon-fetcher/pull/94))\n\n**v3.9.0 (released 2025-07-25):**\n\n- Added driver for the DuckDuckGo icons API. ([#88](https://github.com/ash-jc-allen/favicon-fetcher/pull/88))\n\n**v3.8.0 (released 2025-02-24):**\n\n- Added support for Laravel 12. ([#82](https://github.com/ash-jc-allen/favicon-fetcher/pull/82))\n- Added support for PHPUnit 11. ([#83](https://github.com/ash-jc-allen/favicon-fetcher/pull/83))\n- Migrated from `nunomaduro/larastan` to `larastan/larastan`. ([#84](https://github.com/ash-jc-allen/favicon-fetcher/pull/84))\n- Added support for Larastan 3. ([#84](https://github.com/ash-jc-allen/favicon-fetcher/pull/84))\n\n**v3.7.0 (released 2024-11-30):**\n\n- Added explicit nullable types to support PHP 8.4. ([#80](https://github.com/ash-jc-allen/favicon-fetcher/pull/80))\n\n**v3.6.0 (released 2024-07-08):**\n\n- Added support for `symfony/dom-crawler` v7.0. ([#79](https://github.com/ash-jc-allen/favicon-fetcher/pull/79))\n\n**v3.5.0 (released 2024-06-14):**\n\n- Added a new `verify_tls` config option to disable TLS certificate verification. ([#78](https://github.com/ash-jc-allen/favicon-fetcher/pull/78))\n\n**v3.4.1 (released 2024-04-30):**\n\n- Fixed a bug that prevented fetching icons from a URL if the HTML contained a `link` tag without a `href` attribute.([#77](https://github.com/ash-jc-allen/favicon-fetcher/pull/77))\n\n**v3.4.0 (released 2024-03-19):**\n\n- Added support for `nesbot/carbon 3.0`. ([#76](https://github.com/ash-jc-allen/favicon-fetcher/pull/76))\n\n**v3.3.0 (released 2024-03-12):**\n\n- Added support for Laravel 11. ([#75](https://github.com/ash-jc-allen/favicon-fetcher/pull/75))\n\n**v3.2.0 (released 2024-01-29):**\n\n- Added a `largestByFileSize` method to the `FaviconCollection`. [#73](https://github.com/ash-jc-allen/favicon-fetcher/pull/73)\n\n**v3.1.0 (released 2023-11-07):**\n\n- Added `user_agent` config field to configure HTTP `User-Agent` request header. ([#70](https://github.com/ash-jc-allen/favicon-fetcher/pull/70))\n- Run CI tests using PHP 8.3 ([#69](https://github.com/ash-jc-allen/favicon-fetcher/pull/69))\n\n**v3.0.0 (released 2023-09-04):**\n\n- Added `connect_timeout` and `timeout` config fields. ([#67](https://github.com/ash-jc-allen/favicon-fetcher/pull/67))\n- Use `symfony/dom-crawler` in the `HttpDriver` to parse the HTML. ([#56](https://github.com/ash-jc-allen/favicon-fetcher/pull/56))\n- Updated all files to use strict types for improved type safety. ([#62](https://github.com/ash-jc-allen/favicon-fetcher/pull/62))\n- Throw package-specific exceptions instead of vendor exceptions. ([#67](https://github.com/ash-jc-allen/favicon-fetcher/pull/67))\n- Fixed a bug that prevented an exception from being thrown when using `fetchAll` if no favicons were found when using the `throw` method. ([#56](https://github.com/ash-jc-allen/favicon-fetcher/pull/50))\n- Fixed a bug that prevented the `fetchAll` method from trying to guess the default icon if no favicons were found. ([#56](https://github.com/ash-jc-allen/favicon-fetcher/pull/50))\n- Fixed a bug that stripped the port from the base URL. Thanks for the fix, @mhoffmann777! ([#50](https://github.com/ash-jc-allen/favicon-fetcher/pull/50))\n- Dropped support for PHP 8.0. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))\n- Dropped support for Laravel 8. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))\n- Dropped support for PHPUnit 8.* and Larastan 1.*. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))\n\n**v2.0.0 (released 2023-03-23):**\n- Added driver for the [Favicon Grabber API](https://favicongrabber.com/). ([#24](https://github.com/ash-jc-allen/favicon-fetcher/pull/24))\n- Added `fetchAll` implementation to the `HttpDriver` for fetching all the icons for a URL. ([#29](https://github.com/ash-jc-allen/favicon-fetcher/pull/29), [#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))\n- Added `fetchAll` method to the `AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher` interface. ([#29](https://github.com/ash-jc-allen/favicon-fetcher/pull/29))\n- Added support to get a favicons size and type. ([#29](https://github.com/ash-jc-allen/favicon-fetcher/pull/29), [#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))\n- Changed visibility of the `buildCacheKey` method in the `BuildsCacheKey` trait from `protected` to `public`. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))\n- Changed the values that are used when caching a favicon. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))\n- Removed the `makeFromCache` method from the `Favicon` class. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))\n\n**v1.3.0 (released 2023-01-12):**\n- Added support for Laravel 10. (([#22](https://github.com/ash-jc-allen/favicon-fetcher/pull/22)))\n\n**v1.2.1 (released 2022-11-08):**\n- Fixed bug that prevented a favicon URL from being detected using the `HttpDriver` if the favicon URL was using single quotes (instead of double quotes). ([#20](https://github.com/ash-jc-allen/favicon-fetcher/pull/20))\n\n**v1.2.0 (released 2022-10-17):**\n- Added support for PHP 8.2. ([#21](https://github.com/ash-jc-allen/favicon-fetcher/pull/21))\n\n**v1.1.3 (released 2022-09-03):**\n- Removed an incorrect mime type from the file extension detection. ([#19](https://github.com/ash-jc-allen/favicon-fetcher/pull/19))\n\n**v1.1.2 (released 2022-07-23):**\n- Fixed bug that was using the incorrect file extension when storing favicons retrieved using the \"google-shared-stuff\", \"unavatar\", and \"favicon-kit\" drivers. ([#17](https://github.com/ash-jc-allen/favicon-fetcher/pull/17))\n\n**v1.1.1 (released 2022-05-10):**\n- Fixed bug that was returning the incorrect favicon URL in the `HttpDriver` if multiple `<link>` elements existed on the same line in the webpage's HTML. ([#13](https://github.com/ash-jc-allen/favicon-fetcher/pull/13))\n\n**v1.1.0 (released 2022-04-27):**\n- Added driver for [Unavatar](https://unavatar). ([#8](https://github.com/ash-jc-allen/favicon-fetcher/pull/8))\n\n**v1.0.0 (released 2022-04-26):**\n- Initial release.\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2022 Ashley Allen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n<img src=\"/docs/logo.png\" alt=\"Favicon Fetcher\" width=\"600\">\n</p>\n\n<p align=\"center\">\n<a href=\"https://packagist.org/packages/ashallendesign/favicon-fetcher\"><img src=\"https://img.shields.io/packagist/v/ashallendesign/favicon-fetcher.svg?style=flat-square\" alt=\"Latest Version on Packagist\"></a>\n<a href=\"https://packagist.org/packages/ashallendesign/favicon-fetcher\"><img src=\"https://img.shields.io/packagist/dt/ashallendesign/favicon-fetcher.svg?style=flat-square\" alt=\"Total Downloads\"></a>\n<a href=\"https://packagist.org/packages/ashallendesign/favicon-fetcher\"><img src=\"https://img.shields.io/packagist/php-v/ashallendesign/favicon-fetcher?style=flat-square\" alt=\"PHP from Packagist\"></a>\n<a href=\"https://github.com/ash-jc-allen/favicon-fetcher/blob/master/LICENSE\"><img src=\"https://img.shields.io/github/license/ash-jc-allen/favicon-fetcher?style=flat-square\" alt=\"GitHub license\"></a>\n</p>\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Installation](#installation)\n    * [Requirements](#requirements)\n    * [Install the Package](#install-the-package)\n    * [Publish the Config](#publish-the-config)\n- [Usage](#usage)\n    * [Fetching Favicons](#fetching-favicons)\n        + [Using the `fetch` Method](#using-the-fetch-method)\n        + [Using the `fetchOr` Method](#using-the-fetchor-method)\n        + [Using the `fetchAll` Method](#using-the-fetchall-method)\n        + [Using the `fetchAllOr` Method](#using-the-fetchallor-method)\n    * [Exceptions](#exceptions)\n    * [Drivers](#drivers)\n    * [Available Drivers](#available-drivers)\n        + [How to Choose a Driver](#how-to-choose-a-driver)\n    * [Choosing a Driver](#choosing-a-driver)\n        + [Fallback Drivers](#fallback-drivers)\n        + [Adding Your Own Driver](#adding-your-own-driver)\n    * [HTTP Timeouts](#http-timeouts)\n    * [TLS Verification](#tls-verification)\n    * [HTTP User Agent](#http-user-agent)\n    * [Storing Favicons](#storing-favicons)\n        + [Using `store`](#using-store)\n        + [Using `storeAs`](#using-storeas)\n    * [Caching Favicons](#caching-favicons)\n    * [Favicon Types](#favicon-types)\n    * [Favicon Sizes](#favicon-sizes)\n- [Testing](#testing)\n- [Security](#security)\n- [Contribution](#contribution)\n- [Changelog](#changelog)\n- [Upgrading](#upgrading)\n- [Credits](#credits)\n- [License](#license)\n\n## Overview\n\nA Laravel package that can be used for fetching favicons from websites.\n\n## Installation\n\n### Requirements\n\nThe package has been developed and tested to work with the following minimum requirements:\n\n- PHP 8.0\n- Laravel 8.0\n\n### Install the Package\n\nYou can install the package via Composer:\n\n```bash\ncomposer require ashallendesign/favicon-fetcher\n```\n\n### Publish the Config\nYou can then publish the package's config file by using the following command:\n\n```bash\nphp artisan vendor:publish --provider=\"AshAllenDesign\\FaviconFetcher\\FaviconFetcherProvider\"\n```\n\n## Usage\n\n### Fetching Favicons\n\nNow that you have the package installed, you can start fetching the favicons from different websites.\n\n#### Using the `fetch` Method\n\nTo fetch a favicon from a website, you can use the `fetch` method which will return an instance of `AshAllenDesign\\FaviconFetcher\\Favicon`:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::fetch('https://ashallendesign.co.uk');\n```\n\n#### Using the `fetchOr` Method\n\nIf you'd like to provide a default value to be used if a favicon cannot be found, you can use the `fetchOr` method.\n\nFor example, if you wanted to use a default icon (`https://example.com/favicon.ico`) if a favicon could not be found, your code could look something like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::fetchOr('https://ashallendesign.co.uk', 'https://example.com/favicon.ico');\n```\n\nThis method also accepts a `Closure` as the second argument if you'd prefer to run some custom logic. The `url` field passed as the first argument to the `fetchOr` method is available to use in the closure. For example, to use a closure, your code could look something like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::fetchOr('https://ashallendesign.co.uk', function ($url) {\n    // Run extra logic here...\n\n    return 'https://example.com/favicon.ico';\n});\n```\n\n#### Using the `fetchAll` Method\n\nThere may be times when you want to retrieve the different sized favicons for a given website. To get the different sized favicons, you can use the `fetchAll` method which will return an instance of `AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection`. This collection contains instances of `AshAllenDesign\\FaviconFetcher\\Favicon`. For example, to get all the favicons for a site, you can use the `fetchAll` method like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicons = Favicon::fetchAll('https://ashallendesign.co.uk');\n```\n\nThe `FaviconCollection` class extends the `Illuminate\\Support\\Collection` class, so you can use all the methods available on the `Collection` class.\n\nIt also includes a `largest` method that you can use to get the favicon with the largest dimensions. It's worth noting that if the size of the favicon is unknown, it will be treated as if it has a size of `0x0px` when determining which is the largest.  For example, you can use the `largest` method like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$largestFavicon = Favicon::fetchAll('https://ashallendesign.co.uk')->largest();\n```\n\nThe `FaviconCollection` also provides a `largestByFileSize` method that you can use to get the favicon with the largest file size. You may want to do this if the package cannot detect the sizes of the icons for a given website, and so it can't detect the largest icon. This method works based on the assumption that the larger the file size, the larger the image dimensions. For example, you can use the `largestByFileSize` method like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$largestFavicon = Favicon::fetchAll('https://ashallendesign.co.uk')->largestByFileSize();\n```\n\nNote: Only the `http` driver supports retrieving all the favicons for a given website. For this reason, the `fetchAll` method does not support fallbacks. Support may be added for other drivers and fallbacks in the future. \n\n#### Using the `fetchAllOr` Method\n\nIf you'd like to provide a default value to be used if all the favicons for a site cannot be found, you can use the `fetchAllOr` method.\n\nFor example, if you wanted to use a default icon (`https://example.com/favicon.ico`) if the favicons could not be found, your code could look something like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::fetchAllOr('https://ashallendesign.co.uk', 'https://example.com/favicon.ico');\n```\n\nThis method also accepts a `Closure` as the second argument if you'd prefer to run some custom logic. The `url` field passed as the first argument to the `fetchAllOr` method is available to use in the closure. For example, to use a closure, your code could look something like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::fetchAllOr('https://ashallendesign.co.uk', function ($url) {\n    // Run extra logic here...\n\n    return 'https://example.com/favicon.ico';\n});\n```\n\n### Exceptions\n\nBy default, if a favicon can't be found for a URL, the `fetch` method will return `null`. However, if you'd prefer an exception to be thrown, you can use the `throw` method available on the `Favicon` facade. This means that if a favicon can't be found, an `AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException` will be thrown.\n\nTo enable exceptions to be thrown, your code could look something like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::throw()->fetch('https://ashallendesign.co.uk');\n```\n\nIf you attempt to fetch a favicon and the request times out or no website is found at the URL, an `AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException` will be thrown. This will be thrown even if the `throw` method has not been used.\n\n### Drivers\n\nFavicon Fetcher provides the functionality to use different drivers for retrieving favicons from websites.\n\n### Available Drivers\n\nBy default, Favicon Fetcher ships with 5 drivers out-the-box: `http`, `google-shared-stuff`, `favicon-kit`, `unavatar`, `favicon-grabber`, `duck-duck-go`.\n\nThe `http` driver fetches favicons by attempting to parse \"icon\" and \"shortcut icon\" link elements from the returned HTML of a webpage. If it can't find one, it will attempt to guess the URL of the favicon based on common defaults.\n\nThe `google-shared-stuff` driver fetches favicons using the [Google Shared Stuff](https://google.com) API.\n\nThe `favicon-kit` driver fetches favicons using the [Favicon Kit](https://faviconkit.com) API.\n\nThe `unavatar` driver fetches favicons using the [Unavatar](https://unavatar.io) API.\n\nThe `favicon-grabber` driver fetches favicons using the [Favicon Grabber](https://favicongrabber.com) API.\n\nThe `duck-duck-go` driver fetches favicons using the [DuckDuckGo Icons](https://duckduckgo.com) API.\n\n#### How to Choose a Driver\n\nIt's important to remember that the `google-shared-stuff`, `favicon-kit`, `unavatar`, `favicon-grabber`, and `duck-duck-go` drivers interact with third-party APIs to retrieve the favicons. So, this means that some data will be shared to external services.\n\nHowever, the `http` driver does not use any external services and directly queries the website that you are trying to fetch the favicon for. Due to the fact that this package is new, it is likely that the `http` driver may not be 100% accurate when trying to fetch favicons from websites. So, theoretically, the `http` driver should provide you with better privacy, but may not be as accurate as the other drivers. \n\n### Choosing a Driver\n\nYou can select which driver to use by default by changing the `default` field in the `favicon-fetcher` config file after you've published it. The package originally ships with the `http` driver enabled as the default driver.\n\nFor example, if you wanted to change your default driver to `favicon-kit`, you could update your `favicon-fetcher` config like so:\n\n```php\nreturn [\n\n    // ...\n        \n    'default' => 'favicon-kit',\n            \n    // ...\n\n]\n```\n\nIf you'd like to set the driver on-the-fly, you can do so by using the `driver` method on the `Favicon` facade. For example, if you wanted to use the `google-shared-stuff` driver, you could do so like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::driver('google-shared-stuff')->fetch('https://ashallendesign.co.uk');\n```\n\n#### Fallback Drivers\n\nThere may be times when a particular driver cannot find a favicon for a website. If this happens, you can fall back and attempt to find it again using a different driver.\n\nFor example, if we wanted to try and fetch the favicon using the `http` driver and then fall back to the `google-shared-stuff` driver if we can't find it, your code could look something like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::withFallback('google-shared-stuff')->fetch('https://ashallendesign.co.uk');\n```\n\n#### Adding Your Own Driver\n\nThere might be times when you want to provide your own custom logic for fetching favicons. To do this, you can build your driver and register it with the package for using.\n\nFirst, you'll need to create your own class and make sure that it implements the `AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher` interface. For example, your class could like this:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\n\nclass MyCustomDriver implements Fetcher\n{\n    public function fetch(string $url): ?Favicon\n    {\n        // Add logic here that attempts to fetch a favicon...\n    }\n\n    public function fetchOr(string $url, mixed $default): mixed\n    {\n        // Add logic here that attempts to fetch a favicon or return a default...\n    }\n}\n```\n\nAfter you've created your new driver, you'll be able to register it with the package using the `extend` method available through the `Favicon` facade. You may want to do this in a service provider so that it is set up and available in the rest of your application.\n\nYou can register your custom driver like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\nFavicon::extend('my-custom-driver', new MyCustomDriver());\n```\n\nNow that you've registered your custom driver, you'll be able to use it for fetching favicons like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::driver('my-custom-driver')->fetch('https://ashallendesign.co.uk');\n```\n\n### HTTP Timeouts\n\nFavicon Fetcher provides the ability for you to set the connection timeout and request timeout for all the drivers.\n\nThe connection timeout is the time that the package will wait for a connection to be made to the website. The request timeout is the time that the package will wait for the website to respond to the request.\n\nTo do this, you can update the `connect_timeout` and `timeout` fields in the `favicon-fetcher.php` config file after you've published it. For example, to set the connection timeout to 5 seconds and the request timeout to 10 seconds, you could update your config file like so:\n\n```php\nreturn [\n\n    // ...\n        \n    'connect_timeout' => 5,\n\n    'timeout' => 10,\n            \n    // ...\n\n]\n```\n\nIf you'd prefer that no timeout be set, you can set the values to `0`.\n\nPlease note that these timeouts are applied to all HTTP requests that Favicon Fetcher makes, regardless of the driver that is being used.\n\n### TLS Verification\n\nFavicon Fetcher uses TLS verification by default, but this can be disabled. This can be useful in development environments or situations where you might be working with self-signed certificates or certificates from an untrusted certificate authority.\n\nYou can disable the verification by updating the `verify_tls` field in the `favicon-fetcher.php` config file after you've published it.\n\n```php\nreturn [\n\n    // ...\n        \n    'verify_tls' => false,\n            \n    // ...\n\n]\n```\n\nOr by updating your `.env` file:\n\n```dotenv\nFAVICON_FETCHER_VERIFY_TLS=false\n```\n\n### HTTP User Agent\n\nYou may find that your requests are sometimes blocked by websites when trying to retrieve a favicon. This may be due to the fact that the default Guzzle `User-Agent` header is passed in the requests.\n\nFavicon Fetcher allows you to set the `User-Agent` header that is used in the package's requests. To do this, you can update the `user_agent` field in the `favicon-fetcher.php` config file after you've published it. For example, to set the `User-Agent` header to `My Custom User Agent`, you could update your config file like so:\n\n```php\nreturn [\n\n    // ...\n        \n    'user_agent' => 'My Custom User Agent',\n            \n    // ...\n\n]\n```\n\nThe `User-Agent` header will be set on all HTTP requests that Favicon Fetcher makes, regardless of the driver that is being used.\n\nThe `user_agent` config field is already configured in the config file to read directly from a `FAVICON_FETCHER_USER_AGENT` field in your `.env` file. So, if you'd prefer to set the `User-Agent` header in your `.env` file, you could do so like this:\n\n```dotenv\nFAVICON_FETCHER_USER_AGENT=\"My Custom User Agent\"\n```\n\n### Storing Favicons\n\nAfter fetching favicons, you might want to store them in your filesystem so that you don't need to fetch them again in the future. Favicon Fetcher provides two methods that you can use for storing the favicons: `store` and `storeAs`.\n\n#### Using `store`\n\nIf you use the `store` method, a filename will automatically be generated for the favicon before storing. The method's first parameter accepts a string and is the directory that the favicon will be stored in. You can store a favicon using your default filesystem disk like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->store('favicons');\n\n// $faviconPath is now equal to: \"/favicons/abc-123.ico\"\n```\n\nIf you'd like to use a different storage disk, you can pass it as an optional second argument to the `store` method. For example, to store the favicon on S3, your code use the following:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->store('favicons', 's3');\n\n// $faviconPath is now equal to: \"/favicons/abc-123.ico\"\n```\n\n#### Using `storeAs`\n\nIf you use the `storeAs` method, you will be able to define the filename that the file will be stored as. The method's first parameter accepts a string and is the directory that the favicon will be stored in. The second parameter specifies the favicon filename (excluding the file extension). You can store a favicon using your default filesystem disk like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->storeAs('favicons', 'ashallendesign');\n\n// $faviconPath is now equal to: \"/favicons/ashallendesign.ico\"\n```\n\nIf you'd like to use a different storage disk, you can pass it as an optional third argument to the `storeAs` method. For example, to store the favicon on S3, your code use the following:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->storeAs('favicons', 'ashallendesign', 's3');\n\n// $faviconPath is now equal to: \"/favicons/ashallendesign.ico\"\n```\n\n### Caching Favicons\n\nAs well as being able to store favicons, the package also allows you to cache the favicon URLs. This can be extremely useful if you don't want to store a local copy of the file and want to use the external version of the favicon that the website uses.\n\nAs a basic example, if you have a page displaying 50 websites and their favicons, we would need to find the favicon's URL on each page load. As can imagine, this would drastically increase the page load time. So, by retrieving the URLs from the cache, it would majorly improve up the page speed.\n\nTo cache a favicon, you can use the `cache` method available on the `Favicon` class. The first parameter accepts a `Carbon\\CarbonInterface` as the cache lifetime. For example, to cache the favicon URL of `https://ashallendesign.co.uk` for 1 day, your code might look something like:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::fetch('https://ashallendesign.co.uk')->cache(now()->addDay());\n```\n\nBy default, the package will always try and resolve the favicon from the cache before attempting to retrieve a fresh version. However, if you want to disable the cache and always retrieve a fresh version, you can use the `useCache` method like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$favicon = Favicon::useCache(false)->fetch('https://ashallendesign.co.uk');\n```\n\nThe package uses `favicon-fetcher` as a prefix for all the cache keys. If you'd like to change this, you can do so by changing the `cache.prefix` field in the `favicon-fethcher` config file. For example, to change the prefix to `my-awesome-prefix`, you could update your config file like so:\n\n```php\nreturn [\n\n    // ...\n        \n    'cache' => [\n        'prefix' => 'my-awesome-prefix',\n    ]\n            \n    // ...\n\n]\n```\n\nThe package also provides the functionality for you to cache collections of favicons that have been retrieved using the `fetchAll` method. You can do this by calling the `cache` method on the `FaviconCollection` class like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconCollection = Favicon::fetchAll('https://ashallendesign.co.uk')->cache(now()->addDay());\n```\n\n### Favicon Types\n\nWhen attempting to retrieve favicons using the `http` driver, we may be able to determine the favicons' type (such as `icon`, `shortcut icon`, or `apple-touch-icon`). To get the type of the favicon, you can use the `getIconType` method like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->getIconType();\n```\n\nThis method can return one of four constants defined on the `Favicon` class: `TYPE_ICON`, `TYPE_SHORTCUT_ICON`, `TYPE_APPLE_TOUCH_ICON`, and `TYPE_ICON_UNKNOWN`.\n\nYou can make use of these constants for things like filtering. For example, if you wanted to get all the icons except the `apple-touch-icon`, you could do the following:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconCollection = Favicon::fetchAll('https://ashallendesign.co.uk');\n\n$faviconCollection->filter(function ($favicon) {\n    return $favicon->getIconType() !== Favicon::TYPE_APPLE_TOUCH_ICON;\n});\n```\n\n### Favicon Sizes\n\nWhen attempting to retrieve favicons using the `http` driver, we may be able to determine the favicons' sizes. To get the size of the favicon, you can use the `getIconSize` method like so:\n\n```php\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\n\n$faviconSize = Favicon::fetch('https://ashallendesign.co.uk')->getIconSize();\n```\n\nIt's assumed that the icons are square, so only a single integer will be returned. For example, if a favicon is 16x16px, then the `getIconSize` method will return `16`. If the size is unknown, `null` will be returned.\n\n## Testing\n\nTo run the package's unit tests, run the following command:\n\n``` bash\ncomposer test\n```\n\nTo run Larastan for the package, run the following command:\n\n```bash\ncomposer larastan\n```\n\n## Security\n\nIf you find any security related issues, please contact me directly at [mail@ashallendesign.co.uk](mailto:mail@ashallendesign.co.uk) to report it.\n\n## Contribution\n\nIf you wish to make any changes or improvements to the package, feel free to make a pull request.\n\nTo contribute to this package, please use the following guidelines before submitting your pull request:\n\n- Write tests for any new functions that are added. If you are updating existing code, make sure that the existing tests\n  pass and write more if needed.\n- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standards.\n- Make all pull requests to the `master` branch.\n\n## Changelog\n\nCheck the [CHANGELOG](CHANGELOG.md) to get more information about the latest changes.\n\n## Upgrading\n\nCheck the [UPGRADE](UPGRADE.md) guide to get more information on how to update this library to newer versions.\n\n## Credits\n\n- [Ash Allen](https://ashallendesign.co.uk)\n- [Jess Pickup](https://jesspickup.co.uk) (Logo)\n- [All Contributors](https://github.com/ash-jc-allen/short-url/graphs/contributors)\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n\n## Support Me\n\nIf you've found this package useful, please consider buying a copy of [Battle Ready Laravel](https://battle-ready-laravel.com) to support me and my work.\n\nEvery sale makes a huge difference to me and allows me to spend more time working on open-source projects and tutorials.\n\nTo say a huge thanks, you can use the code **BATTLE20** to get a 20% discount on the book.\n\n[👉 Get Your Copy!](https://battle-ready-laravel.com)\n\n[![Battle Ready Laravel](https://ashallendesign.co.uk/images/custom/sponsors/battle-ready-laravel-horizontal-banner.png)](https://battle-ready-laravel.com)\n"
  },
  {
    "path": "UPGRADE.md",
    "content": "# Upgrade Guide\n\n## Contents\n\n- [Upgrading from 2.* to 3.0.0](#upgrading-from-2-to-300)\n- [Upgrading from 1.* to 2.0.0](#upgrading-from-1-to-200)\n\n## Upgrading from 2.* to 3.0.0\n\n### Exceptions\n\nPreviously, if Favicon Fetcher attempted to make a request to a URL and the request failed (for example, if the site doesn't exist), an `Illuminate\\Http\\Client\\ConnectionException` would be thrown. However, as of v3.0.0, a `AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException` will be thrown instead.\n\nThis will be thrown in the following situations:\n\n- If the URL has a valid structure but the site doesn't exist.\n- If the HTTP client exceeds the connection timeout.\n- If the HTTP client exceeds the request timeout.\n\n## Upgrading from 1.* to 2.0.0\n\n### Method Visibility Changes\n\nThe visibility of the `buildCacheKey` method in the `AshAllenDesign\\FaviconFetcher\\Concerns\\BuildsCacheKeys` trait has been changed from `protected` to `public`. If you are overriding this method anywhere in your code, you'll need to update the visibility to `public`.\n\n### Added `fetchAll` and `fetchAllOr` Methods to `Fetcher` Interface\n\nThe `fetchAll` and `fetchAllOr` methods have been added to the `AshAllenDesign\\FaviconFetcher\\Interfaces\\Fetcher` interface. If you are implementing this interface in your own code, you'll need to add these method to your implementation.\n\nThe signatures for the new methods are:\n\n```php\npublic function fetchAll(string $url): FaviconCollection;\n```\n\n```php\npublic function fetchAllOr(string $url, mixed $default): mixed;\n```\n\n### Removed `makeFromCache` Method from `Favicon` Class\n\nThe `makeFromCache` method in the `AshAllenDesign\\FaviconFetcher\\Favicon` class has been removed. This method was originally intended as a helper method when first added, but it doesn't provide much value, so it has been removed. \n\nIf you were making use of this method anywhere, you'll need to remove it from your code.\n\n### Caching Changes\n\nPreviously, Favicon Fetcher only stored the URL of the favicon when calling the `cache` method. However, as of v2.0.0, Favicon Fetcher can determine the size and type of favicons, so this information is now stored in the cache as well.\n\nThis means that instead of a string being stored in the cache, an array is now stored instead.\n\nThe package has some minor backwards-compatible support to handle items cached before v2.0.0. If you are attempting to retrieve a cached favicon that was stored in the cache before v2.0.0, the `Favicon` class' type and size won't be set. The size and type will only be available on Favicons that were cached from v2.0.0 onwards.\n\nIn a future release (likely v3.0.0), the backwards-compatible support will be removed so that only arrays can be read from the cache.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"ashallendesign/favicon-fetcher\",\n  \"description\": \"A Laravel package for fetching website's favicons.\",\n  \"type\": \"library\",\n  \"homepage\": \"https://github.com/ash-jc-allen/favicon-fetcher\",\n  \"license\": \"MIT\",\n  \"authors\": [\n    {\n      \"name\": \"Ash Allen\",\n      \"email\": \"mail@ashallendesign.co.uk\"\n    }\n  ],\n  \"keywords\": [\n    \"ashallendesign\",\n    \"favicon-fetcher\",\n    \"favicon\",\n    \"icon\"\n  ],\n  \"require\": {\n    \"php\": \"^8.1\",\n    \"nesbot/carbon\": \"^2.0|^3.0\",\n    \"illuminate/cache\": \"^9.0|^10.0|^11.0|^12.0|^13.0\",\n    \"illuminate/filesystem\": \"^9.0|^10.0|^11.0|^12.0|^13.0\",\n    \"illuminate/http\": \"^9.0|^10.0|^11.0|^12.0|^13.0\",\n    \"guzzlehttp/guzzle\": \"^7.4\",\n    \"symfony/dom-crawler\": \"^6.3 || ^7.0\"\n  },\n  \"require-dev\": {\n    \"mockery/mockery\": \"^1.0\",\n    \"orchestra/testbench\": \"^7.0|^8.0|^9.0|^10.0|^11.0\",\n    \"phpunit/phpunit\": \"^9.0|^10.0|^11.0\",\n    \"larastan/larastan\": \"^2.0|^3.0\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"AshAllenDesign\\\\FaviconFetcher\\\\\": \"src/\"\n    }\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n        \"AshAllenDesign\\\\FaviconFetcher\\\\Tests\\\\\": \"tests/\"\n    }\n  },\n  \"extra\": {\n    \"laravel\": {\n      \"providers\": [\n        \"AshAllenDesign\\\\FaviconFetcher\\\\FaviconFetcherProvider\"\n      ]\n    }\n  },\n  \"scripts\": {\n    \"test\": \"vendor/bin/phpunit\",\n    \"larastan\": \"vendor/bin/phpstan analyse\"\n  },\n  \"minimum-stability\": \"dev\",\n  \"prefer-stable\": true\n}\n"
  },
  {
    "path": "config/favicon-fetcher.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Driver\n    |--------------------------------------------------------------------------\n    |\n    | The driver that should be used by default for fetching the favicons.\n    | By default, the package comes with support for several drivers,\n    | but you can define your own if needed.\n    |\n    | Supported drivers: \"http\", \"google-shared-stuff\", \"favicon-kit\",\n    |                    \"unavatar\", \"favicon-grabber\"\n    |\n    */\n    'default' => 'http',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Caching\n    |--------------------------------------------------------------------------\n    |\n    | The package provides support for caching the fetched favicon's URLs.\n    | Here, you can specify the different options for caching, such as\n    | cache prefix that is prepended to all the cache keys.\n    |\n    */\n    'cache' => [\n        'prefix' => 'favicon-fetcher',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | HTTP Timeouts\n    |--------------------------------------------------------------------------\n    |\n    | Set the timeouts here in seconds for the HTTP requests that are made\n    | to fetch the favicons. If the timeout is set to 0, then no timeout\n    | will be applied. The connect timeout is the time taken to connect\n    | to the server, while the timeout is the time taken to get a\n    | response from the server after the connection is made.\n    |\n    */\n    'timeout' => 0,\n\n    'connect_timeout' => 0,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Verify TLS\n    |--------------------------------------------------------------------------\n    |\n    | Sets the TLS verification option when making HTTP requests, which is\n    | enabled by default.\n    */\n    'verify_tls' => env('FAVICON_FETCHER_VERIFY_TLS', true),\n\n    /*\n    |--------------------------------------------------------------------------\n    | HTTP User Agent\n    |--------------------------------------------------------------------------\n    |\n    | Set the user agent used by the HTTP client when fetching the favicons.\n    |\n    */\n    'user_agent' => env('FAVICON_FETCHER_USER_AGENT'),\n];\n"
  },
  {
    "path": "phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n\nparameters:\n\n    paths:\n        - src\n\n    level: 6\n\n    ignoreErrors:\n        - \"#^Unsafe usage of new static#\"\n        - '#^Call to an undefined method AshAllenDesign\\\\FaviconFetcher\\\\Collections\\\\FaviconCollection::fetch\\(\\)#'\n        - '#^Call to an undefined method AshAllenDesign\\\\FaviconFetcher\\\\Collections\\\\FaviconCollection::fetchAll\\(\\)#'\n\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"vendor/autoload.php\"\n         backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         colors=\"true\"\n         verbose=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\">\n    <testsuites>\n        <testsuite name=\"Test Suite\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n    <filter>\n        <whitelist>\n            <directory suffix=\".php\">src/</directory>\n        </whitelist>\n    </filter>\n    <php>\n        <env name=\"APP_ENV\" value=\"testing\"/>\n    </php>\n</phpunit>\n"
  },
  {
    "path": "src/Collections/FaviconCollection.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Collections;\n\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Carbon\\CarbonInterface;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\n\n/**\n * @extends Collection<int, Favicon>\n */\nclass FaviconCollection extends Collection\n{\n    use HasDefaultFunctionality;\n\n    /**\n     * Whether the favicons in this collection were all retrieved from the cache.\n     */\n    protected bool $retrievedFromCache = false;\n\n    /**\n     * @param  array<array<int,string>>  $items\n     * @return static\n     */\n    public static function makeFromCache(array $items = []): static\n    {\n        $collection = new static($items);\n\n        $collection->retrievedFromCache = true;\n\n        return $collection;\n    }\n\n    /**\n     * Cache the collection of favicons. We only cache the collection if it contains\n     * items and if it was not retrieved from the cache. If the collection was\n     * retrieved from the cache, then the \"force\" flag has to be set to\n     * true in order to cache it.\n     */\n    public function cache(CarbonInterface $ttl, bool $force = false): self\n    {\n        $shouldCache = $this->isNotEmpty()\n            && ($force || ! $this->retrievedFromCache);\n\n        if ($shouldCache) {\n            $cacheKey = $this->buildCacheKeyForCollection($this->first()->getUrl());\n\n            $cacheData = $this->map(fn (Favicon $favicon): array => $favicon->toCache())->all();\n\n            Cache::put($cacheKey, $cacheData, $ttl);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Get the favicon with the largest icon size. Any icons with an unknown size (null)\n     * will be treated as having a size of 0.\n     */\n    public function largest(): ?Favicon\n    {\n        return $this->sortByDesc(\n            fn (Favicon $favicon): ?int => $favicon->getIconSize()\n        )->first();\n    }\n\n    public function largestByFileSize(): ?Favicon\n    {\n        return $this->sortByDesc(\n            fn (Favicon $favicon): int => strlen($favicon->content())\n        )->first();\n    }\n}\n"
  },
  {
    "path": "src/Concerns/BuildsCacheKeys.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\ntrait BuildsCacheKeys\n{\n    /**\n     * Build the key used for caching the favicon.\n     *\n     * @param  string  $url\n     * @return string\n     */\n    public function buildCacheKey(string $url): string\n    {\n        $url = str_replace(['http://', 'https://'], '', $url);\n\n        return config('favicon-fetcher.cache.prefix').'.'.$url;\n    }\n\n    /**\n     * Build the key used for caching the favicon collection.\n     *\n     * @param  string  $url\n     * @return string\n     */\n    public function buildCacheKeyForCollection(string $url): string\n    {\n        return $this->buildCacheKey($url).'.collection';\n    }\n}\n"
  },
  {
    "path": "src/Concerns/HasDefaultFunctionality.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\Favicon as FetchedFavicon;\nuse Illuminate\\Support\\Facades\\Cache;\n\ntrait HasDefaultFunctionality\n{\n    use BuildsCacheKeys;\n\n    /**\n     * An array of the drivers that should be as fallbacks if the current\n     * driver fails to retrieve a favicon for the given URL.\n     *\n     * @var string[]\n     */\n    protected array $fallbacks = [];\n\n    /**\n     * Whether to throw an exception if the favicon cannot be found.\n     *\n     * @var bool\n     */\n    protected bool $throwOnNotFound = false;\n\n    /**\n     * Whether to attempt to retrieve the favicon URL from the cache.\n     *\n     * @var bool\n     */\n    protected bool $useCache = true;\n\n    /**\n     * Attempt to fetch the favicon for the given URL. If a favicon cannot\n     * be found, return the default as a fallback.\n     *\n     * @param  string  $url\n     * @param  mixed  $default\n     * @return mixed\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidUrlException\n     */\n    public function fetchOr(string $url, mixed $default): mixed\n    {\n        if ($favicon = $this->fetch($url)) {\n            return $favicon;\n        }\n\n        return $default instanceof \\Closure ? $default($url) : $default;\n    }\n\n    /**\n     * Attempt to fetch all the favicons for the given URL. If the favicons cannot\n     * be found, return the default as a fallback.\n     *\n     * @param  string  $url\n     * @param  mixed  $default\n     * @return mixed\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidUrlException\n     * @throws FeatureNotSupportedException\n     */\n    public function fetchAllOr(string $url, mixed $default): mixed\n    {\n        $favicons = $this->fetchAll($url);\n\n        if ($favicons->isNotEmpty()) {\n            return $favicons;\n        }\n\n        return $default instanceof \\Closure ? $default($url) : $default;\n    }\n\n    /**\n     * Specify whether to throw an exception if the favicon cannot be found.\n     *\n     * @param  bool  $throw\n     * @return $this\n     */\n    public function throw(bool $throw = true): self\n    {\n        $this->throwOnNotFound = $throw;\n\n        return $this;\n    }\n\n    /**\n     * Specify which drivers should be used as fallbacks if the current\n     * driver cannot find the favicon.\n     *\n     * @param  string  ...$fallbacks\n     * @return $this\n     */\n    public function withFallback(string ...$fallbacks): self\n    {\n        $this->fallbacks = array_merge($this->fallbacks, $fallbacks);\n\n        return $this;\n    }\n\n    /**\n     * Specify whether to attempt to read the favicon from the cache.\n     *\n     * @param  bool  $useCache\n     * @return $this\n     */\n    public function useCache(bool $useCache = true): self\n    {\n        $this->useCache = $useCache;\n\n        return $this;\n    }\n\n    /**\n     * Handle what happens if the favicon cannot be found using the current\n     * driver. If any fallbacks are specified, attempt to find a favicon\n     * using a different driver. If we have specified to throw an\n     * exception, then do so. Otherwise, return null.\n     *\n     * @param  string  $url\n     * @return FetchedFavicon|null\n     *\n     * @throws FaviconNotFoundException\n     */\n    protected function notFound(string $url)\n    {\n        if ($favicon = $this->attemptFallbacks($url)) {\n            return $favicon;\n        }\n\n        if ($this->throwOnNotFound) {\n            throw new FaviconNotFoundException('A favicon cannot be found for '.$url);\n        }\n\n        return null;\n    }\n\n    /**\n     * Loop through each fallback driver and attempt to retrieve a favicon.\n     *\n     * @param  string  $url\n     * @return FetchedFavicon|null\n     */\n    protected function attemptFallbacks(string $url): ?FetchedFavicon\n    {\n        foreach ($this->fallbacks as $driver) {\n            if ($favicon = Favicon::driver($driver)->fetch($url)) {\n                return $favicon;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Return the cached favicon, if one exists, or return null.\n     *\n     * @param  string  $url\n     * @return FetchedFavicon|null\n     *\n     * @throws FaviconFetcherException\n     */\n    protected function attemptToFetchFromCache(string $url): ?FetchedFavicon\n    {\n        $cachedFaviconData = Cache::get($this->buildCacheKey($url));\n\n        if (! $cachedFaviconData) {\n            return null;\n        }\n\n        // If the cached data is still stored in the older format used in\n        // v1 of the package, then we convert it to the new format. In\n        // v3 of the package, we will remove this check and enforce\n        // an array to be stored.\n        if (is_string($cachedFaviconData)) {\n            $cachedFaviconData = [\n                'favicon_url' => $cachedFaviconData,\n                'icon_type' => FetchedFavicon::TYPE_ICON_UNKNOWN,\n                'icon_size' => null,\n            ];\n        }\n\n        return (new FetchedFavicon(\n            url: $url,\n            faviconUrl: $cachedFaviconData['favicon_url'],\n            retrievedFromCache: true,\n        ))\n            ->setIconType($cachedFaviconData['icon_type'])\n            ->setIconSize($cachedFaviconData['icon_size']);\n    }\n\n    /**\n     * Return a collection of cached favicons if they exist, or return null.\n     *\n     * @param  string  $url\n     * @return FaviconCollection|null\n     *\n     * @throws FaviconFetcherException\n     */\n    protected function attemptToFetchCollectionFromCache(string $url): ?FaviconCollection\n    {\n        $cachedFaviconsData = Cache::get($this->buildCacheKeyForCollection($url));\n\n        if (! $cachedFaviconsData) {\n            return null;\n        }\n\n        $favicons = new FaviconCollection();\n\n        foreach ($cachedFaviconsData as $cachedFaviconData) {\n            $favicons->push((new FetchedFavicon(\n                url: $url,\n                faviconUrl: $cachedFaviconData['favicon_url'],\n                retrievedFromCache: true,\n            ))\n                ->setIconType($cachedFaviconData['icon_type'])\n                ->setIconSize($cachedFaviconData['icon_size']));\n        }\n\n        return $favicons;\n    }\n}\n"
  },
  {
    "path": "src/Concerns/MakesHttpRequests.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse Illuminate\\Http\\Client\\ConnectionException as ClientConnectionException;\nuse Illuminate\\Http\\Client\\PendingRequest;\nuse Illuminate\\Support\\Facades\\Http;\n\ntrait MakesHttpRequests\n{\n    protected function httpClient(): PendingRequest\n    {\n        $client = Http::timeout(config('favicon-fetcher.timeout'))\n            ->connectTimeout(config('favicon-fetcher.connect_timeout'));\n\n        if ($userAgent = config('favicon-fetcher.user_agent')) {\n            $client->withUserAgent($userAgent);\n        }\n\n        if (! config('favicon-fetcher.verify_tls')) {\n            $client->withoutVerifying();\n        }\n\n        return $client;\n    }\n\n    protected function withRequestExceptionHandling(\\Closure $callback): mixed\n    {\n        try {\n            return $callback();\n        } catch (ClientConnectionException $exception) {\n            throw new ConnectionException(\n                $exception->getMessage(),\n                $exception->getCode(),\n                $exception\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Concerns/ValidatesUrls.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\ntrait ValidatesUrls\n{\n    /**\n     * Validate that the given parameter is a valid URL.\n     *\n     * @param  string  $url\n     * @return bool\n     */\n    protected function urlIsValid(string $url): bool\n    {\n        return filter_var($url, FILTER_VALIDATE_URL) !== false;\n    }\n}\n"
  },
  {
    "path": "src/Contracts/Fetcher.php",
    "content": "<?php\n\nnamespace AshAllenDesign\\FaviconFetcher\\Contracts;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\n\ninterface Fetcher\n{\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     */\n    public function fetch(string $url): ?Favicon;\n\n    /**\n     * Attempt to fetch all favicons and icons for the given URL.\n     *\n     * @param  string  $url\n     * @return FaviconCollection\n     */\n    public function fetchAll(string $url): FaviconCollection;\n\n    /**\n     * Attempt to fetch the favicon for the given URL. If a favicon cannot\n     * be found, return the default as a fallback.\n     *\n     * @param  string  $url\n     * @param  mixed  $default\n     * @return mixed\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidUrlException\n     */\n    public function fetchOr(string $url, mixed $default): mixed;\n\n    /**\n     * Attempt to fetch all the favicons for the given URL. If the favicons cannot\n     * be found, return the default as a fallback.\n     *\n     * @param  string  $url\n     * @param  mixed  $default\n     * @return mixed\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidUrlException\n     * @throws FeatureNotSupportedException\n     */\n    public function fetchAllOr(string $url, mixed $default): mixed;\n}\n"
  },
  {
    "path": "src/Drivers/DuckDuckGoDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\ValidatesUrls;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Illuminate\\Http\\Client\\Response;\n\nclass DuckDuckGoDriver implements Fetcher\n{\n    use ValidatesUrls;\n    use HasDefaultFunctionality;\n    use MakesHttpRequests;\n\n    private const BASE_URL = 'https://icons.duckduckgo.com/ip3/';\n\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidUrlException\n     * @throws ConnectionException\n     * @throws FaviconFetcherException\n     */\n    public function fetch(string $url): ?Favicon\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {\n            return $favicon;\n        }\n\n        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);\n\n        $faviconUrl = self::BASE_URL.$urlWithoutProtocol.'.ico';\n\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($faviconUrl)\n        );\n\n        return $response->successful()\n            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)\n            : $this->notFound($url);\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        throw new FeatureNotSupportedException('The DuckDuckGo driver does not support fetching all favicons.');\n    }\n}\n"
  },
  {
    "path": "src/Drivers/FaviconGrabberDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\ValidatesUrls;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Illuminate\\Http\\Client\\Response;\n\nclass FaviconGrabberDriver implements Fetcher\n{\n    use ValidatesUrls;\n    use HasDefaultFunctionality;\n    use MakesHttpRequests;\n\n    private const BASE_URL = 'https://favicongrabber.com/api/grab/';\n\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws InvalidUrlException\n     * @throws FaviconNotFoundException\n     * @throws ConnectionException\n     * @throws FaviconFetcherException\n     */\n    public function fetch(string $url): ?Favicon\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {\n            return $favicon;\n        }\n\n        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);\n\n        $apiUrl = self::BASE_URL.$urlWithoutProtocol;\n\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($apiUrl)\n        );\n\n        if (! $response->successful() || count($response->json('icons')) === 0) {\n            return $this->notFound($url);\n        }\n\n        $faviconUrl = $response->json('icons')[0]['src'];\n\n        return new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this);\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        throw new FeatureNotSupportedException('The FaviconGrabber driver does not support fetching all favicons.');\n    }\n}\n"
  },
  {
    "path": "src/Drivers/FaviconKitDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\ValidatesUrls;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Illuminate\\Http\\Client\\Response;\n\nclass FaviconKitDriver implements Fetcher\n{\n    use ValidatesUrls;\n    use HasDefaultFunctionality;\n    use MakesHttpRequests;\n\n    private const BASE_URL = 'https://api.faviconkit.com/';\n\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws InvalidUrlException\n     * @throws FaviconNotFoundException\n     * @throws FaviconFetcherException\n     */\n    public function fetch(string $url): ?Favicon\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {\n            return $favicon;\n        }\n\n        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);\n\n        $faviconUrl = self::BASE_URL.$urlWithoutProtocol;\n\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($faviconUrl)\n        );\n\n        return $response->successful()\n            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)\n            : $this->notFound($url);\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        throw new FeatureNotSupportedException('The FaviconKit API does not support fetching all favicons.');\n    }\n}\n"
  },
  {
    "path": "src/Drivers/GoogleSharedStuffDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\ValidatesUrls;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Illuminate\\Http\\Client\\Response;\n\nclass GoogleSharedStuffDriver implements Fetcher\n{\n    use ValidatesUrls;\n    use HasDefaultFunctionality;\n    use MakesHttpRequests;\n\n    private const BASE_URL = 'https://www.google.com/s2/favicons?domain=';\n\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidUrlException\n     * @throws ConnectionException\n     * @throws FaviconFetcherException\n     */\n    public function fetch(string $url): ?Favicon\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {\n            return $favicon;\n        }\n\n        $faviconUrl = self::BASE_URL.$url;\n\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($faviconUrl)\n        );\n\n        return $response->successful()\n            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)\n            : $this->notFound($url);\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        throw new FeatureNotSupportedException('The Google Shared Stuff API does not support fetching all favicons.');\n    }\n}\n"
  },
  {
    "path": "src/Drivers/HttpDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\ValidatesUrls;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidIconSizeException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidIconTypeException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Illuminate\\Http\\Client\\Response;\nuse Symfony\\Component\\DomCrawler\\Crawler;\n\nclass HttpDriver implements Fetcher\n{\n    use ValidatesUrls;\n    use HasDefaultFunctionality;\n    use MakesHttpRequests;\n\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws FaviconNotFoundException\n     * @throws InvalidIconSizeException\n     * @throws InvalidIconTypeException\n     * @throws InvalidUrlException\n     * @throws FaviconFetcherException\n     */\n    public function fetch(string $url): ?Favicon\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {\n            return $favicon;\n        }\n\n        $favicon = $this->attemptToResolveFromHeadTags($url)\n            ?? new Favicon(url: $url, faviconUrl: $this->guessDefaultUrl($url), fromDriver: $this);\n\n        $faviconCanBeReached = $this->faviconUrlCanBeReached($favicon->getFaviconUrl());\n\n        return $faviconCanBeReached\n            ? $favicon\n            : $this->notFound($url);\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicons = $this->attemptToFetchCollectionFromCache($url)) {\n            return $favicons;\n        }\n\n        $favicons = $this->attemptToResolveAllFromHeadTags($url);\n\n        // If the URL couldn't be reached, throw and exception and return\n        // an empty FaviconCollection.\n        if ($favicons === null) {\n            if ($this->throwOnNotFound) {\n                throw new FaviconNotFoundException('A favicon cannot be found for '.$url);\n            }\n\n            $favicons = new FaviconCollection();\n        }\n\n        if ($favicons->isEmpty()) {\n            $favicons->push(new Favicon(url: $url, faviconUrl: $this->guessDefaultUrl($url), fromDriver: $this));\n        }\n\n        // Return a FaviconCollection of favicons that can be reached.\n        return $favicons->filter(\n            fn (Favicon $favicon): bool => $this->faviconUrlCanBeReached($favicon->getFaviconUrl())\n        );\n    }\n\n    /**\n     * Attempt to resolve a favicon from the given URL. If the response\n     * is successful, we can assume that a valid favicon was returned.\n     * Otherwise, we can assume that a favicon wasn't found.\n     *\n     * @param  string  $faviconUrl\n     * @return bool\n     *\n     * @throws ConnectionException\n     */\n    private function faviconUrlCanBeReached(string $faviconUrl): bool\n    {\n        return $this->withRequestExceptionHandling(\n            fn (): bool => $this->httpClient()\n                ->get($faviconUrl)\n                ->successful()\n        );\n    }\n\n    /**\n     * Parse the HTML returned from the URL and attempt to find a favicon\n     * specified using the \"icon\" or \"shortcut icon\" link tag. If one\n     * is found, return the absolute URL of the link's \"href\".\n     * Otherwise, return null.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws InvalidIconSizeException\n     * @throws InvalidIconTypeException\n     * @throws ConnectionException\n     */\n    private function attemptToResolveFromHeadTags(string $url): ?Favicon\n    {\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($url)\n        );\n\n        if (! $response->successful()) {\n            return null;\n        }\n\n        $linkTag = (new Crawler($response->body()))\n            ->filter('\n                head link[rel=\"icon\"][href],\n                head link[rel=\"shortcut icon\"][href]\n            ')\n            ->first();\n\n        if (! $linkTag->count()) {\n            return null;\n        }\n\n        $favicon = new Favicon(\n            url: $url,\n            faviconUrl: $this->convertToAbsoluteUrl($url, $linkTag->attr('href')),\n            fromDriver: $this,\n        );\n\n        if ($iconSize = $linkTag->attr('sizes')) {\n            $favicon->setIconSize((int) $iconSize);\n        }\n\n        if ($iconType = $this->guessTypeFromElement($linkTag)) {\n            $favicon->setIconType($iconType);\n        }\n\n        return $favicon;\n    }\n\n    /**\n     * @throws ConnectionException\n     */\n    private function attemptToResolveAllFromHeadTags(string $url): ?FaviconCollection\n    {\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($url)\n        );\n\n        if (! $response->successful()) {\n            return null;\n        }\n\n        $linkTags = (new Crawler($response->body()))\n            ->filter('\n                head link[rel=\"icon\"][href],\n                head link[rel=\"shortcut icon\"][href],\n                head link[rel=\"apple-touch-icon\"][href]\n            ');\n\n        if (! $linkTags->count()) {\n            return null;\n        }\n\n        $favicons = $linkTags->each(function (Crawler $linkTag) use ($url): Favicon {\n            $favicon = new Favicon(\n                $url,\n                $this->convertToAbsoluteUrl($url, $linkTag->attr('href')),\n                $this,\n            );\n\n            if ($iconSize = $linkTag->attr('sizes')) {\n                $favicon->setIconSize((int) $iconSize);\n            }\n\n            if ($iconType = $this->guessTypeFromElement($linkTag)) {\n                $favicon->setIconType($iconType);\n            }\n\n            return $favicon;\n        });\n\n        return new FaviconCollection($favicons);\n    }\n\n    private function guessTypeFromElement(Crawler $linkElement): string\n    {\n        return match ($linkElement->attr('rel')) {\n            'icon' => Favicon::TYPE_ICON,\n            'shortcut icon' => Favicon::TYPE_SHORTCUT_ICON,\n            'apple-touch-icon' => Favicon::TYPE_APPLE_TOUCH_ICON,\n            default => Favicon::TYPE_ICON_UNKNOWN,\n        };\n    }\n\n    /**\n     * Convert the favicon URL to be absolute rather than relative.\n     *\n     * @param  string  $baseUrl\n     * @param  string  $faviconUrl\n     * @return string\n     */\n    private function convertToAbsoluteUrl(string $baseUrl, string $faviconUrl): string\n    {\n        // If the favicon URL is relative, we need to convert it to be absolute.\n        // We also strip the path (if there is one) from the base URL.\n        if (! filter_var($faviconUrl, FILTER_VALIDATE_URL)) {\n            $faviconUrl = $this->stripPathFromUrl($baseUrl).'/'.ltrim($faviconUrl, '/');\n        }\n\n        return $faviconUrl;\n    }\n\n    /**\n     * Build and return the default path where we can guess the favicon\n     * file might be stored.\n     *\n     * @param  string  $url\n     * @return string\n     */\n    private function guessDefaultUrl(string $url): string\n    {\n        return rtrim($this->stripPathFromUrl($url)).'/favicon.ico';\n    }\n\n    /**\n     * Strip the path and any query parameters from the given URL so that\n     * we only return the scheme, host and port (if there is one).\n     *\n     * @param  string  $url\n     * @return string\n     */\n    private function stripPathFromUrl(string $url): string\n    {\n        $parsedUrl = parse_url($url);\n\n        $url = $parsedUrl['scheme'].'://'.$parsedUrl['host'];\n\n        if (array_key_exists('port', $parsedUrl)) {\n            $url .= ':'.$parsedUrl['port'];\n        }\n\n        return $url;\n    }\n}\n"
  },
  {
    "path": "src/Drivers/UnavatarDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\HasDefaultFunctionality;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\ValidatesUrls;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FeatureNotSupportedException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Illuminate\\Http\\Client\\Response;\n\nclass UnavatarDriver implements Fetcher\n{\n    use ValidatesUrls;\n    use HasDefaultFunctionality;\n    use MakesHttpRequests;\n\n    private const BASE_URL = 'https://unavatar.io/';\n\n    /**\n     * Attempt to fetch the favicon for the given URL.\n     *\n     * @param  string  $url\n     * @return Favicon|null\n     *\n     * @throws InvalidUrlException\n     * @throws FaviconNotFoundException\n     * @throws FaviconFetcherException\n     */\n    public function fetch(string $url): ?Favicon\n    {\n        if (! $this->urlIsValid($url)) {\n            throw new InvalidUrlException($url.' is not a valid URL');\n        }\n\n        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {\n            return $favicon;\n        }\n\n        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);\n\n        $faviconUrl = self::BASE_URL.$urlWithoutProtocol.'?fallback=false';\n\n        $response = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($faviconUrl)\n        );\n\n        return $response->successful()\n            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)\n            : $this->notFound($url);\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        throw new FeatureNotSupportedException('The Unavatar API does not support fetching all favicons.');\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/ConnectionException.php",
    "content": "<?php\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass ConnectionException extends FaviconFetcherException\n{\n    //\n}\n"
  },
  {
    "path": "src/Exceptions/FaviconFetcherException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nuse Exception;\n\nclass FaviconFetcherException extends Exception\n{\n    //\n}\n"
  },
  {
    "path": "src/Exceptions/FaviconNotFoundException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass FaviconNotFoundException extends FaviconFetcherException\n{\n    //\n}\n"
  },
  {
    "path": "src/Exceptions/FeatureNotSupportedException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass FeatureNotSupportedException extends FaviconFetcherException\n{\n    //\n}\n"
  },
  {
    "path": "src/Exceptions/InvalidIconSizeException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass InvalidIconSizeException extends FaviconFetcherException\n{\n    //\n}\n"
  },
  {
    "path": "src/Exceptions/InvalidIconTypeException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass InvalidIconTypeException extends FaviconFetcherException\n{\n    //\n}\n"
  },
  {
    "path": "src/Exceptions/InvalidUrlException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass InvalidUrlException extends FaviconFetcherException\n{\n    //\n}\n"
  },
  {
    "path": "src/Facades/Favicon.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Facades;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Favicon as FetchedFavicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse Illuminate\\Support\\Facades\\Facade;\n\n/**\n * @method static Fetcher driver(string $driver = null)\n * @method static void extend(string $name, Fetcher $fetcher)\n * @method static Fetcher throw(bool $throw = true)\n * @method static Fetcher withFallback(string ...$fallbacks)\n * @method static Fetcher useCache(bool $useCache = true)\n * @method static FetchedFavicon|null fetch(string $url)\n * @method static mixed fetchOr(string $url, mixed $default)\n * @method static FaviconCollection fetchAll(string $url)\n * @method static mixed fetchAllOr(string $url, mixed $default)\n *\n * @see FetcherManager\n */\nclass Favicon extends Facade\n{\n    /**\n     * Get the registered name of the component.\n     *\n     * @return string\n     */\n    protected static function getFacadeAccessor(): string\n    {\n        return 'favicon-fetcher';\n    }\n}\n"
  },
  {
    "path": "src/Favicon.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher;\n\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\BuildsCacheKeys;\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidIconSizeException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidIconTypeException;\nuse Carbon\\CarbonInterface;\nuse Illuminate\\Http\\Client\\Response;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\File;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Illuminate\\Support\\Str;\n\nclass Favicon\n{\n    use BuildsCacheKeys;\n    use MakesHttpRequests;\n\n    public const TYPE_ICON = 'icon';\n\n    public const TYPE_SHORTCUT_ICON = 'shortcut_icon';\n\n    public const TYPE_APPLE_TOUCH_ICON = 'apple_touch_icon';\n\n    public const TYPE_ICON_UNKNOWN = 'unknown';\n\n    /**\n     * The URL of the website that the favicon belongs to.\n     *\n     * @var string\n     */\n    protected string $url;\n\n    /**\n     * The URL of the favicon.\n     *\n     * @var string\n     */\n    protected string $faviconUrl;\n\n    /**\n     * The driver that was used to fetch the favicon. If the favicon was\n     * retrieved from the cache, this will be null.\n     *\n     * @var Fetcher|null\n     */\n    protected ?Fetcher $driver = null;\n\n    /**\n     * Whether the favicon's URL was retrieved from the cache.\n     *\n     * @var bool\n     */\n    protected bool $retrievedFromCache = false;\n\n    protected string $iconType = self::TYPE_ICON_UNKNOWN;\n\n    protected ?int $size = null;\n\n    public function __construct(\n        string $url,\n        string $faviconUrl,\n        ?Fetcher $fromDriver = null,\n        bool $retrievedFromCache = false\n    ) {\n        $this->url = $url;\n        $this->faviconUrl = $faviconUrl;\n        $this->driver = $fromDriver;\n        $this->retrievedFromCache = $retrievedFromCache;\n    }\n\n    public function setIconSize(?int $size): static\n    {\n        if ($size !== null && $size < 0) {\n            throw new InvalidIconSizeException('The size ['.$size.'] is not a valid favicon size.');\n        }\n\n        $this->size = $size;\n\n        return $this;\n    }\n\n    public function setIconType(string $type): static\n    {\n        if (! $this->acceptableIconType($type)) {\n            throw new InvalidIconTypeException('The type ['.$type.'] is not a valid favicon type.');\n        }\n\n        $this->iconType = $type;\n\n        return $this;\n    }\n\n    public function getUrl(): string\n    {\n        return $this->url;\n    }\n\n    public function getFaviconUrl(): string\n    {\n        return $this->faviconUrl;\n    }\n\n    public function retrievedFromCache(): bool\n    {\n        return $this->retrievedFromCache;\n    }\n\n    /**\n     * Get the contents of the favicon file.\n     *\n     * @return string\n     *\n     * @throws ConnectionException\n     */\n    public function content(): string\n    {\n        return $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($this->faviconUrl)\n        )->body();\n    }\n\n    /**\n     * Cache the favicon URL. If the favicon is already cached, \"force\"\n     * must be passed as \"true\" to re-cache the URL.\n     *\n     * @param  CarbonInterface  $ttl\n     * @param  bool  $force\n     * @return $this\n     */\n    public function cache(CarbonInterface $ttl, bool $force = false): self\n    {\n        if ($force || ! $this->retrievedFromCache) {\n            Cache::put(\n                $this->buildCacheKey($this->url),\n                $this->toCache(),\n                $ttl\n            );\n        }\n\n        return $this;\n    }\n\n    /**\n     * Store the favicon in storage using an automatically generate filename.\n     *\n     * @param  string  $directory\n     * @param  string|null  $disk\n     * @return string\n     */\n    public function store(string $directory, ?string $disk = null): string\n    {\n        return $this->storeAs($directory, Str::uuid()->toString(), $disk);\n    }\n\n    /**\n     * Store the favicon in storage.\n     *\n     * @param  string  $directory\n     * @param  string  $filename\n     * @param  string|null  $disk\n     * @return string\n     */\n    public function storeAs(string $directory, string $filename, ?string $disk = null): string\n    {\n        $path = $this->buildStoragePath($directory, $filename);\n\n        Storage::disk($disk)->put($path, $this->content());\n\n        return $path;\n    }\n\n    public function getIconType(): string\n    {\n        return $this->iconType;\n    }\n\n    public function getIconSize(): ?int\n    {\n        return $this->size;\n    }\n\n    protected function buildStoragePath(string $directory, string $filename): string\n    {\n        return Str::of($directory)\n            ->append('/')\n            ->append($filename)\n            ->append('.')\n            ->append($this->guessFileExtension())\n            ->toString();\n    }\n\n    protected function guessFileExtension(): string\n    {\n        $default = File::extension($this->faviconUrl);\n\n        if (Str::of($this->faviconUrl)->endsWith(['png', 'ico', 'svg'])) {\n            return $default;\n        }\n\n        return $this->guessFileExtensionFromMimeType() ?? $default;\n    }\n\n    /**\n     * @throws ConnectionException\n     */\n    protected function guessFileExtensionFromMimeType(): ?string\n    {\n        $faviconMimetype = $this->withRequestExceptionHandling(\n            fn (): Response => $this->httpClient()->get($this->faviconUrl)\n        )->header('content-type');\n\n        $mimeToExtensionMap = [\n            'image/x-icon' => 'ico',\n            'image/x-ico' => 'ico',\n            'image/vnd.microsoft.icon' => 'ico',\n            'image/jpeg' => 'jpeg',\n            'image/pjpeg' => 'jpeg',\n            'image/png' => 'png',\n            'image/x-png' => 'png',\n            'image/svg+xml' => 'svg',\n        ];\n\n        return $mimeToExtensionMap[$faviconMimetype] ?? null;\n    }\n\n    private function acceptableIconType(string $type): bool\n    {\n        return in_array(\n            needle: $type,\n            haystack: [\n                self::TYPE_ICON,\n                self::TYPE_SHORTCUT_ICON,\n                self::TYPE_APPLE_TOUCH_ICON,\n                self::TYPE_ICON_UNKNOWN,\n            ],\n            strict: true);\n    }\n\n    /**\n     * Transform the favicon object into an array that can be cached.\n     *\n     * @return array<string,string|int|null>\n     */\n    public function toCache(): array\n    {\n        return [\n            'favicon_url' => $this->getFaviconUrl(),\n            'icon_size' => $this->getIconSize(),\n            'icon_type' => $this->getIconType(),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/FaviconFetcherProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher;\n\nuse Illuminate\\Support\\ServiceProvider;\n\nclass FaviconFetcherProvider extends ServiceProvider\n{\n    /**\n     * Register any application services.\n     *\n     * @return void\n     */\n    public function register(): void\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/favicon-fetcher.php', 'favicon-fetcher');\n\n        $this->app->bind('favicon-fetcher', fn () => new FetcherManager());\n    }\n\n    /**\n     * Bootstrap any application services.\n     *\n     * @return void\n     */\n    public function boot(): void\n    {\n        $this->publishes([\n            __DIR__.'/../config/favicon-fetcher.php' => config_path('favicon-fetcher.php'),\n        ], 'favicon-fetcher-config');\n    }\n}\n"
  },
  {
    "path": "src/FetcherManager.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher;\n\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\DuckDuckGoDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\FaviconGrabberDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\FaviconKitDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\GoogleSharedStuffDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\HttpDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\UnavatarDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\n\nclass FetcherManager\n{\n    /**\n     * @var Fetcher[]\n     */\n    protected static array $customDrivers = [];\n\n    public static function driver(?string $driver = null): Fetcher\n    {\n        $driver ??= config('favicon-fetcher.default');\n\n        return match ($driver) {\n            'http' => new HttpDriver(),\n            'google-shared-stuff' => new GoogleSharedStuffDriver(),\n            'favicon-kit' => new FaviconKitDriver(),\n            'unavatar' => new UnavatarDriver(),\n            'favicon-grabber' => new FaviconGrabberDriver(),\n            'duck-duck-go' => new DuckDuckGoDriver(),\n            default => static::attemptToCreateCustomDriver($driver),\n        };\n    }\n\n    public static function extend(string $name, Fetcher $fetcher): void\n    {\n        self::$customDrivers[$name] = $fetcher;\n    }\n\n    protected static function attemptToCreateCustomDriver(string $driver): Fetcher\n    {\n        return static::$customDrivers[$driver]\n            ?? throw new FaviconFetcherException($driver.' is not a valid driver.');\n    }\n\n    public function __call(string $method, mixed $parameters): mixed\n    {\n        return static::driver()->$method(...$parameters);\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Collections/FaviconCollectionTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Collections;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Cache;\n\nfinal class FaviconCollectionTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /** @test */\n    public function favicon_collection_can_be_cached_if_the_collection_was_not_retrieved_from_the_cache(): void\n    {\n        $collection = FaviconCollection::make([\n            (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n        ]);\n\n        $collection->cache(now()->addDay());\n\n        $cachedItems = Cache::get('favicon-fetcher.example.com.collection');\n\n        self::assertSame(\n            expected: [\n                [\n                    'favicon_url' => 'https://example.com/images/apple-icon-180x180.png',\n                    'icon_size' => 180,\n                    'icon_type' => 'apple_touch_icon',\n                ],\n                [\n                    'favicon_url' => 'https://example.com/images/favicon.ico',\n                    'icon_size' => null,\n                    'icon_type' => 'shortcut_icon',\n                ],\n            ],\n            actual: $cachedItems\n        );\n    }\n\n    /** @test */\n    public function favicon_collection_can_be_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_true(): void\n    {\n        Cache::put(\n            key: 'favicon-fetcher.example.com.collection',\n            value: 'Dummy value here that should be overridden',\n            ttl: now()->addDay(),\n        );\n\n        FaviconCollection::makeFromCache([\n            (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n        ])->cache(now()->addDay(), true);\n\n        // Assert that the items in the database were overridden.\n        self::assertSame(\n            expected: [\n                [\n                    'favicon_url' => 'https://example.com/images/apple-icon-180x180.png',\n                    'icon_size' => 180,\n                    'icon_type' => 'apple_touch_icon',\n                ],\n                [\n                    'favicon_url' => 'https://example.com/images/favicon.ico',\n                    'icon_size' => null,\n                    'icon_type' => 'shortcut_icon',\n                ],\n            ],\n            actual: Cache::get('favicon-fetcher.example.com.collection')\n        );\n    }\n\n    /** @test */\n    public function favicon_collection_is_not_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_false(): void\n    {\n        Cache::put(\n            key: 'favicon-fetcher.example.com.collection',\n            value: 'Dummy value here that should not be overridden',\n            ttl: now()->addDay(),\n        );\n\n        FaviconCollection::makeFromCache([\n            (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n        ])->cache(now()->addDay());\n\n        // Assert that the items in the database were not overridden.\n        self::assertSame(\n            expected: 'Dummy value here that should not be overridden',\n            actual: Cache::get('favicon-fetcher.example.com.collection')\n        );\n    }\n\n    /** @test */\n    public function favicon_collection_is_not_cached_if_the_collection_is_empty(): void\n    {\n        Cache::shouldReceive('put')->never();\n\n        $collection = new FaviconCollection();\n\n        $collection->cache(now()->addDay());\n    }\n\n    /** @test */\n    public function largest_favicon_can_be_retrieved(): void\n    {\n        $largest = FaviconCollection::make([\n            (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-57x57.png'))->setIconSize(57)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-60x60.png'))->setIconSize(60)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(72)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(76)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-76x76.png'))->setIconSize(114)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-120x120.png'))->setIconSize(120)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-144x144.png'))->setIconSize(144)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-152x152.png'))->setIconSize(152)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/android-icon-192x192.png'))->setIconSize(192)->setIconType(Favicon::TYPE_ICON),\n        ])->largest();\n\n        self::assertSame('https://example.com/favicon/android-icon-192x192.png', $largest->getFaviconUrl());\n    }\n\n    /** @test */\n    public function largest_favicon_can_be_retrieved_if_there_are_only_null_sizes(): void\n    {\n        $largest = FaviconCollection::make([\n            (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),\n            (new Favicon('https://example.com', 'https://example.com/favicon/favicon-64x64.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),\n        ])->largest();\n\n        self::assertSame('https://example.com/favicon/favicon-32x32.png', $largest->getFaviconUrl());\n    }\n\n    /** @test */\n    public function largest_favicon_can_be_retrieved_based_on_file_size()\n    {\n        // mock the favicons to specify file content lengths\n        $favicon1 = $this->createMock(Favicon::class);\n        $favicon1->method('getFaviconUrl')->willReturn('https://example.com/favicon/favicon-32x32.png');\n        $favicon1->method('content')->willReturn('some-short-string');\n\n        $favicon2 = $this->createMock(Favicon::class);\n        $favicon2->method('getFaviconUrl')->willReturn('https://example.com/favicon/favicon-64x64.png');\n        $favicon2->method('content')->willReturn('some-much-longer-string');\n\n        $largest = FaviconCollection::make([\n            $favicon1,\n            $favicon2,\n        ])->largestByFileSize();\n\n        self::assertSame('https://example.com/favicon/favicon-64x64.png', $largest->getFaviconUrl());\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Concerns/MakesHttpRequests/HttpClientTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Concerns\\MakesHttpRequests;\n\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\n\nfinal class HttpClientTest extends TestCase\n{\n    use MakesHttpRequests;\n\n    /** @test */\n    public function http_client_is_returned_with_correct_options(): void\n    {\n        config([\n            'favicon-fetcher.timeout' => 10,\n            'favicon-fetcher.connect_timeout' => 5,\n            'favicon-fetcher.verify_tls' => false,\n        ]);\n\n        $client = $this->httpClient();\n\n        self::assertEquals(10, $client->getOptions()['timeout']);\n        self::assertEquals(5, $client->getOptions()['connect_timeout']);\n        self::assertFalse($client->getOptions()['verify']);\n    }\n\n    /** @test */\n    public function http_client_is_returned_with_correct_verify_tls_option(): void\n    {\n        config([\n            'favicon-fetcher.verify_tls' => true,\n        ]);\n\n        $client = $this->httpClient();\n\n        // The \"verify\" option shouldn't be present because we've not set it,\n        // so if it's not in the array, we can make the assumption that it's\n        // set to true under the hood by Laravel.\n        self::assertArrayNotHasKey('verify', $client->getOptions());\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Concerns/MakesHttpRequests/WithRequestExceptionHandlingTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Concerns\\MakesHttpRequests;\n\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\MakesHttpRequests;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\ConnectionException;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Http\\Client\\ConnectionException as ClientConnectionException;\n\nfinal class WithRequestExceptionHandlingTest extends TestCase\n{\n    use MakesHttpRequests;\n\n    /** @test */\n    public function exception_is_handled_and_rethrown(): void\n    {\n        $this->expectException(ConnectionException::class);\n\n        $this->withRequestExceptionHandling(function () {\n            throw new ClientConnectionException('Test exception');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Drivers/DuckDuckGoDriverTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\DuckDuckGoDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\NullDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass DuckDuckGoDriverTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /**\n     * @test\n     *\n     * @testWith [\"https\"]\n     *           [\"http\"]\n     */\n    public function favicon_can_be_fetched_from_driver(string $protocol): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new DuckDuckGoDriver())->fetch($protocol.'://example.com');\n\n        self::assertSame('https://icons.duckduckgo.com/ip3/example.com.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com',\n            [\n                'favicon_url' => 'url-goes-here',\n                'icon_size' => null,\n                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n            ],\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new DuckDuckGoDriver())->fetch('https://example.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.https://example.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new DuckDuckGoDriver())->useCache(false)->fetch('https://example.com');\n\n        self::assertSame('https://icons.duckduckgo.com/ip3/example.com.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new DuckDuckGoDriver())->useCache(true)->fetch('https://example.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new CustomDriver());\n\n        $favicon = (new DuckDuckGoDriver())\n            ->withFallback('custom-driver')\n            ->useCache(true)\n            ->fetch('https://example.com');\n\n        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new DuckDuckGoDriver())\n                ->throw()\n                ->useCache(true)\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method(): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new DuckDuckGoDriver())\n            ->useCache(true)\n            ->fetchOr('https://example.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new DuckDuckGoDriver())\n            ->fetchOr('https://example.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function exception_can_be_thrown_after_attempting_a_fallback(): void\n    {\n        Http::fake([\n            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new NullDriver());\n\n        $exception = null;\n\n        try {\n            (new DuckDuckGoDriver())\n                ->throw()\n                ->withFallback('custom-driver')\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n\n        self::assertTrue(NullDriver::$flag);\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_url_is_invalid(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new DuckDuckGoDriver())->fetch('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Drivers/FaviconGrabberDriverTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\FaviconGrabberDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\NullDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass FaviconGrabberDriverTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /**\n     * @test\n     *\n     * @testWith [\"https\"]\n     *           [\"http\"]\n     */\n    public function favicon_can_be_fetched_from_driver(string $protocol): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/aws.amazon.com' => Http::response($this->successfulResponseBody()),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())->fetch($protocol.'://aws.amazon.com');\n\n        self::assertSame('https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.aws.amazon.com',\n            [\n                'favicon_url' => 'url-goes-here',\n                'icon_size' => null,\n                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n            ],\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())->fetch('https://aws.amazon.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.https://aws.amazon.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            'https://favicongrabber.com/api/grab/aws.amazon.com' => Http::response($this->successfulResponseBody()),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())->useCache(false)->fetch('https://aws.amazon.com');\n\n        self::assertSame('https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/empty.com' => Http::response($this->successfulEmptyResponseBody()),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())->useCache(true)->fetch('https://empty.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_domain_is_invalid(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())->useCache(true)->fetch('https://invalid.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new CustomDriver());\n\n        $favicon = (new FaviconGrabberDriver())\n            ->withFallback('custom-driver')\n            ->useCache(true)\n            ->fetch('https://invalid.com');\n\n        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new FaviconGrabberDriver())\n                ->throw()\n                ->useCache(true)\n                ->fetch('https://invalid.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://invalid.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())\n            ->useCache(true)\n            ->fetchOr('https://invalid.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconGrabberDriver())\n            ->fetchOr('https://invalid.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function exception_can_be_thrown_after_attempting_a_fallback(): void\n    {\n        Http::fake([\n            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new NullDriver());\n\n        $exception = null;\n\n        try {\n            (new FaviconGrabberDriver())\n                ->throw()\n                ->withFallback('custom-driver')\n                ->fetch('https://invalid.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://invalid.com', $exception->getMessage());\n\n        self::assertTrue(NullDriver::$flag);\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_url_is_invalid(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new FaviconGrabberDriver())->fetch('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n\n    private function successfulResponseBody(): array\n    {\n        return [\n            'domain' => 'aws.amazon.com',\n            'icons' => [\n                [\n                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico',\n                    'type' => 'image/ico',\n                ],\n                [\n                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico',\n                    'type' => 'image/ico',\n                ],\n                [\n                    'sizes' => '57x57',\n                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-iphone-114-smile.png',\n                ],\n                [\n                    'sizes' => '72x72',\n                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-ipad-144-smile.png',\n                ],\n                [\n                    'sizes' => '114x114',\n                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-iphone-114-smile.png',\n                ],\n                [\n                    'sizes' => '144x144',\n                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-ipad-144-smile.png',\n                ],\n                [\n                    'src' => 'https://aws.amazon.com/favicon.ico',\n                    'type' => 'image/x-icon',\n                ],\n            ],\n        ];\n    }\n\n    private function successfulEmptyResponseBody(): array\n    {\n        return [\n            'domain' => 'empty.com',\n            'icons' => [],\n        ];\n    }\n\n    private function domainNotFoundResponseBody(): array\n    {\n        return [\n            'error' => 'Unresolved domain name.',\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Drivers/FaviconKitDriverTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\FaviconKitDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\NullDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass FaviconKitDriverTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /**\n     * @test\n     *\n     * @testWith [\"https\"]\n     *           [\"http\"]\n     */\n    public function favicon_can_be_fetched_from_driver(string $protocol): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconKitDriver())->fetch($protocol.'://example.com');\n\n        self::assertSame('https://api.faviconkit.com/example.com', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com',\n            [\n                'favicon_url' => 'url-goes-here',\n                'icon_size' => null,\n                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n            ],\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconKitDriver())->fetch('https://example.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.https://example.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconKitDriver())->useCache(false)->fetch('https://example.com');\n\n        self::assertSame('https://api.faviconkit.com/example.com', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconKitDriver())->useCache(true)->fetch('https://example.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new CustomDriver());\n\n        $favicon = (new FaviconKitDriver())\n            ->withFallback('custom-driver')\n            ->useCache(true)\n            ->fetch('https://example.com');\n\n        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new FaviconKitDriver())\n                ->throw()\n                ->useCache(true)\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method(): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconKitDriver())\n            ->useCache(true)\n            ->fetchOr('https://example.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new FaviconKitDriver())\n            ->fetchOr('https://example.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function exception_can_be_thrown_after_attempting_a_fallback(): void\n    {\n        Http::fake([\n            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new NullDriver());\n\n        $exception = null;\n\n        try {\n            (new FaviconKitDriver())\n                ->throw()\n                ->withFallback('custom-driver')\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n\n        self::assertTrue(NullDriver::$flag);\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_url_is_invalid(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new FaviconKitDriver())->fetch('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Drivers/GoogleSharedStuffDriverTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\GoogleSharedStuffDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\NullDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass GoogleSharedStuffDriverTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /**\n     * @test\n     *\n     * @testWith [\"https\"]\n     *           [\"http\"]\n     */\n    public function favicon_can_be_fetched_from_driver(string $protocol): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain='.$protocol.'://example.com' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new GoogleSharedStuffDriver())->fetch($protocol.'://example.com');\n\n        self::assertSame('https://www.google.com/s2/favicons?domain='.$protocol.'://example.com', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com',\n            [\n                'favicon_url' => 'url-goes-here',\n                'icon_size' => null,\n                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n            ],\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new GoogleSharedStuffDriver())->fetch('https://example.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.https://example.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new GoogleSharedStuffDriver())->useCache(false)->fetch('https://example.com');\n\n        self::assertSame('https://www.google.com/s2/favicons?domain=https://example.com', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new GoogleSharedStuffDriver())->useCache(true)->fetch('https://example.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new CustomDriver());\n\n        $favicon = (new GoogleSharedStuffDriver())\n            ->withFallback('custom-driver')\n            ->useCache(true)\n            ->fetch('https://example.com');\n\n        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new GoogleSharedStuffDriver())\n                ->throw()\n                ->useCache(true)\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method(): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new GoogleSharedStuffDriver())\n            ->useCache(true)\n            ->fetchOr('https://example.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new GoogleSharedStuffDriver())\n            ->fetchOr('https://example.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function exception_can_be_thrown_after_attempting_a_fallback(): void\n    {\n        Http::fake([\n            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new NullDriver());\n\n        $exception = null;\n\n        try {\n            (new GoogleSharedStuffDriver())\n                ->throw()\n                ->withFallback('custom-driver')\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n\n        self::assertTrue(NullDriver::$flag);\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_url_is_invalid(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new GoogleSharedStuffDriver())->fetch('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Drivers/HttpDriverTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\HttpDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\NullDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Http\\Client\\Request;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass HttpDriverTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /**\n     * @test\n     *\n     * @dataProvider faviconLinksInHtmlProvider\n     */\n    public function favicon_can_be_fetched_using_link_element_in_html(\n        string $html,\n        string $expectedFaviconUrl,\n        ?int $expectedSize,\n        string $expectedType,\n    ): void {\n        Http::preventStrayRequests();\n\n        Http::fake([\n            'https://example.com' => Http::response($html),\n            $expectedFaviconUrl => Http::response('favicon contents here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com');\n\n        self::assertSame($expectedFaviconUrl, $favicon->getFaviconUrl());\n        self::assertSame($expectedSize, $favicon->getIconSize());\n        self::assertSame($expectedType, $favicon->getIconType());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_if_the_url_has_a_path_and_thelink_element_contains_a_relative_url(): void\n    {\n        Http::fake([\n            'https://example.com/blog' => Http::response(self::htmlOptionOne()),\n            'https://example.com/icon/is/here.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com/blog');\n\n        self::assertSame('https://example.com/icon/is/here.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html(): void\n    {\n        $responseHtml = <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"localization\" href=\"branding/brand.ftl\" />\n            </html>\n        HTML;\n\n        Http::fake([\n            'https://example.com' => Http::response($responseHtml),\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com');\n\n        self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_and_a_relative_url_is_passed(): void\n    {\n        $responseHtml = <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"localization\" href=\"branding/brand.ftl\" />\n            </html>\n        HTML;\n\n        Http::fake([\n            'https://example.com/blog' => Http::response($responseHtml),\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com/blog');\n\n        self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /**\n     * @test\n     *\n     * @testWith [\"https\"]\n     *           [\"http\"]\n     */\n    public function favicon_can_be_fetched_from_driver(string $protocol): void\n    {\n        Http::fake([\n            'https://example.com' => Http::response('<link href=\"/icon/favicon.ico\" rel=\"icon\">'),\n            'http://example.com' => Http::response('<link href=\"/icon/favicon.ico\" rel=\"icon\">'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch($protocol.'://example.com');\n\n        self::assertSame($protocol.'://example.com/icon/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_url_with_port(): void\n    {\n        Http::fake([\n            'http://example.com:8080' => Http::response('<link href=\"/icon/favicon.ico\" rel=\"icon\">'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('http://example.com:8080');\n\n        self::assertSame('http://example.com:8080/icon/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_url_with_query_parameters(): void\n    {\n        Http::fake([\n            'http://example.com?query=parameter' => Http::response('<link href=\"/icon/favicon.ico\" rel=\"icon\">'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('http://example.com?query=parameter');\n\n        self::assertSame('http://example.com/icon/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com',\n            [\n                'favicon_url' => 'url-goes-here',\n                'icon_size' => null,\n                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n            ],\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n        self::assertNull($favicon->getIconSize());\n        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicon->getIconType());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists_in_the_old_string_format(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n        self::assertNull($favicon->getIconSize());\n        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicon->getIconType());\n    }\n\n    /** @test */\n    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.https://example.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            'https://example.com' => Http::response('<link href=\"/icon/favicon.ico\" rel=\"icon\">'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->useCache(false)->fetch('https://example.com');\n\n        self::assertSame('https://example.com/icon/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())->useCache(true)->fetch('https://example.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new CustomDriver());\n\n        $favicon = (new HttpDriver())\n            ->withFallback('custom-driver')\n            ->useCache(true)\n            ->fetch('https://example.com');\n\n        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new HttpDriver())\n                ->throw()\n                ->useCache(true)\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())\n            ->useCache(true)\n            ->fetchOr('https://example.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())\n            ->fetchOr('https://example.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function exception_can_be_thrown_after_attempting_a_fallback(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new NullDriver());\n\n        $exception = null;\n\n        try {\n            (new HttpDriver())\n                ->throw()\n                ->withFallback('custom-driver')\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n\n        self::assertTrue(NullDriver::$flag);\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_url_is_invalid(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new HttpDriver())->fetch('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider allFaviconLinksInHtmlProvider\n     */\n    public function all_icons_for_a_url_can_be_fetched(string $html, $expectedFaviconCollection): void\n    {\n        Http::fake([\n            'https://example.com' => Http::response($html),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAll('https://example.com');\n\n        self::assertCount($expectedFaviconCollection->count(), $favicons);\n\n        foreach ($favicons as $index => $favicon) {\n            self::assertSame($expectedFaviconCollection[$index]->getFaviconUrl(), $favicon->getFaviconUrl());\n            self::assertSame($expectedFaviconCollection[$index]->getIconType(), $favicon->getIconType());\n            self::assertSame($expectedFaviconCollection[$index]->getIconSize(), $favicon->getIconSize());\n        }\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_when_trying_to_get_all_icons(): void\n    {\n        $responseHtml = <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"localization\" href=\"branding/brand.ftl\" />\n            </html>\n        HTML;\n\n        Http::fake([\n            'https://example.com' => Http::response($responseHtml),\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAll('https://example.com');\n\n        self::assertCount(1, $favicons);\n        self::assertSame($favicons->first()->getFaviconUrl(), 'https://example.com/favicon.ico');\n    }\n\n    /** @test */\n    public function empty_favicon_collection_is_returned_if_the_url_cannot_be_reached(): void\n    {\n        Http::fake([\n            'https://example.com' => Http::response('not found', 404),\n            'https://example.com/favicon.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAll('https://example.com');\n\n        self::assertCount(0, $favicons);\n    }\n\n    /** @test */\n    public function empty_favicon_collection_is_returned_if_no_icons_can_be_found_for_a_url(): void\n    {\n        $responseHtml = <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"localization\" href=\"branding/brand.ftl\" />\n            </html>\n        HTML;\n\n        Http::fake([\n            'https://example.com' => Http::response($responseHtml),\n            'https://example.com/favicon.ico' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAll('https://example.com');\n\n        self::assertCount(0, $favicons);\n    }\n\n    /** @test */\n    public function error_is_thrown_if_trying_to_find_all_the_favicons_for_an_invalid_url(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new HttpDriver())->fetchAll('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n\n    /** @test */\n    public function error_is_thrown_if_no_icons_can_be_found_for_a_url_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        $responseHtml = <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"localization\" href=\"branding/brand.ftl\" />\n            </html>\n        HTML;\n\n        Http::fake([\n            'https://example.com' => Http::response($responseHtml),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new HttpDriver())\n                ->throw()\n                ->fetchAll('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function all_favicon_for_a_url_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com.collection',\n            [\n                [\n                    'favicon_url' => 'url-goes-here',\n                    'icon_size' => null,\n                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n                ],\n                [\n                    'favicon_url' => 'url-goes-here-1',\n                    'icon_size' => 100,\n                    'icon_type' => Favicon::TYPE_ICON,\n                ],\n                [\n                    'favicon_url' => 'url-goes-here-1.com',\n                    'icon_size' => 192,\n                    'icon_type' => Favicon::TYPE_APPLE_TOUCH_ICON,\n                ],\n            ],\n            now()->addHour(),\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAll('https://example.com');\n\n        self::assertCount(3, $favicons);\n\n        self::assertSame('url-goes-here', $favicons->first()->getFaviconUrl());\n        self::assertSame('url-goes-here-1', $favicons->skip(1)->first()->getFaviconUrl());\n        self::assertSame('url-goes-here-1.com', $favicons->skip(2)->first()->getFaviconUrl());\n\n        self::assertNull($favicons->first()->getIconSize());\n        self::assertSame(100, $favicons->skip(1)->first()->getIconSize());\n        self::assertSame(192, $favicons->skip(2)->first()->getIconSize());\n\n        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicons->first()->getIconType());\n        self::assertSame(Favicon::TYPE_ICON, $favicons->skip(1)->first()->getIconType());\n        self::assertSame(Favicon::TYPE_APPLE_TOUCH_ICON, $favicons->skip(2)->first()->getIconType());\n    }\n\n    /** @test */\n    public function all_favicons_for_a_url_are_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com.collection',\n            [\n                [\n                    'favicon_url' => 'url-goes-here',\n                    'icon_size' => null,\n                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n                ],\n                [\n                    'favicon_url' => 'url-goes-here-1',\n                    'icon_size' => 100,\n                    'icon_type' => Favicon::TYPE_ICON,\n                ],\n                [\n                    'favicon_url' => 'url-goes-here-1.com',\n                    'icon_size' => 192,\n                    'icon_type' => Favicon::TYPE_APPLE_TOUCH_ICON,\n                ],\n            ],\n            now()->addHour(),\n        );\n\n        Http::fake([\n            'https://example.com' => Http::response('<link href=\"/icon/favicon.ico\" rel=\"icon\">'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->useCache(false)->fetchAll('https://example.com');\n\n        self::assertCount(1, $favicons);\n\n        self::assertSame('https://example.com/icon/favicon.ico', $favicons->first()->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicons_can_be_returned_using_the_fetchAllOr_method(): void\n    {\n        Http::fake([\n            'https://example.com' => Http::response(self::htmlOptionOne()),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAllOr('https://example.com', 'should not fallback to this');\n\n        self::assertCount(1, $favicons);\n\n        self::assertSame('https://example.com/icon/is/here.ico', $favicons->first()->getFaviconUrl());\n        self::assertSame(Favicon::TYPE_ICON, $favicons->first()->getIconType());\n        self::assertSame(null, $favicons->first()->getIconSize());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchAllOr_method(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())\n            ->useCache(true)\n            ->fetchAllOr('https://example.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchAllOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://example.com/*' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new HttpDriver())\n            ->fetchAllOr('https://example.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function can_set_the_user_agent_when_fetching()\n    {\n        Http::fake();\n\n        $driver = new HttpDriver();\n\n        // No user agent set.\n        $driver->fetch('https://example.com');\n\n        Http::assertSent(function (Request $request) {\n            return $request->hasHeader('User-Agent', 'GuzzleHttp/7');\n        });\n\n        // Custom user agent.\n        config()->set('favicon-fetcher.user_agent', 'test-user-agent');\n\n        $driver->fetch('https://example.com');\n\n        Http::assertSent(function (Request $request) {\n            return $request->hasHeader('User-Agent', 'test-user-agent');\n        });\n    }\n\n    /** @test */\n    public function null_is_returned_if_using_fetch_and_the_link_has_no_href(): void\n    {\n        $responseHtml = <<<'HTML'\n            <head>\n                <meta charset=\"utf-8\" />\n                <title>Dummy title</title>\n                <meta name=\"title\" content=\"Dummy title\">\n                <meta name=\"description\" content=\"Dummy title\">\n                <link rel=\"icon\" type=\"image/x-icon\">\n            </head>\n        HTML;\n\n        Http::preventStrayRequests();\n\n        Http::fake([\n            'https://example.com' => Http::response($responseHtml),\n            'https://example.com/favicon.ico' => Http::response(status: 404),\n        ]);\n\n        $favicon = (new HttpDriver())->fetch('https://example.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function null_is_returned_if_using_fetchAll_and_the_link_has_no_href(): void\n    {\n        $responseHtml = <<<'HTML'\n            <head>\n                <meta charset=\"utf-8\" />\n                <title>Dummy title</title>\n                <meta name=\"title\" content=\"Dummy title\">\n                <meta name=\"description\" content=\"Dummy title\">\n                <link rel=\"icon\" type=\"image/x-icon\">\n            </head>\n        HTML;\n\n        Http::preventStrayRequests();\n\n        Http::fake([\n            'https://example.com' => Http::response($responseHtml),\n            'https://example.com/favicon.ico' => Http::response(status: 404),\n        ]);\n\n        $favicons = (new HttpDriver())->fetchAll('https://example.com');\n\n        self::assertCount(0, $favicons);\n    }\n\n    public static function allFaviconLinksInHtmlProvider(): array\n    {\n        return [\n            [\n                self::htmlOptionOne(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionTwo(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionThree(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionFour(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-57x57.png'))->setIconSize(57)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-60x60.png'))->setIconSize(60)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(72)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(76)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-76x76.png'))->setIconSize(114)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-120x120.png'))->setIconSize(120)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-144x144.png'))->setIconSize(144)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-152x152.png'))->setIconSize(152)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/favicon/android-icon-192x192.png'))->setIconSize(192)->setIconType(Favicon::TYPE_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionFive(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionSix(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionSeven(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionEight(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionNine(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionTen(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://www.example.com/favicon123.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),\n                    (new Favicon('https://example.com', 'https://www.example.com/favicon123.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),\n                ]),\n            ],\n            [\n                self::htmlOptionEleven(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-57x57.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(57),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-60x60.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(60),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-72x72.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(72),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-76x76.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(76),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-114x114.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(114),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-120x120.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(120),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-144x144.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(144),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-152x152.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(152),\n                    (new Favicon('https://example.com', 'https://example.com/apple-icon-200x200.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(200),\n                    (new Favicon('https://example.com', 'https://example.com/android-icon-192x192.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(192),\n                    (new Favicon('https://example.com', 'https://example.com/favicon-32x32.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(32),\n                    (new Favicon('https://example.com', 'https://example.com/favicon-96x96.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(96),\n                ]),\n            ],\n            [\n                self::htmlOptionThirteen(),\n                FaviconCollection::make([\n                    (new Favicon('https://example.com', 'https://example.com/favicon-96x96.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(96),\n                ]),\n            ],\n        ];\n    }\n\n    public static function faviconLinksInHtmlProvider(): array\n    {\n        return [\n            [self::htmlOptionOne(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_ICON],\n            [self::htmlOptionTwo(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_ICON],\n            [self::htmlOptionThree(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_SHORTCUT_ICON],\n            [self::htmlOptionFour(), 'https://example.com/favicon/favicon-32x32.png', null, Favicon::TYPE_ICON],\n            [self::htmlOptionFive(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_SHORTCUT_ICON],\n            [self::htmlOptionSix(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_SHORTCUT_ICON],\n            [self::htmlOptionSeven(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_SHORTCUT_ICON],\n            [self::htmlOptionEight(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_ICON],\n            [self::htmlOptionNine(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_ICON],\n            [self::htmlOptionTen(), 'https://www.example.com/favicon123.ico', null, Favicon::TYPE_SHORTCUT_ICON],\n            [self::htmlOptionEleven(), 'https://example.com/android-icon-192x192.png', 192, Favicon::TYPE_ICON],\n            [self::htmlOptionTwelve(), 'https://example.com/android-icon-192x192.png', 192, Favicon::TYPE_ICON],\n            [self::htmlOptionThirteen(), 'https://example.com/favicon-96x96.png', 96, Favicon::TYPE_ICON],\n        ];\n    }\n\n    private static function htmlOptionOne(): string\n    {\n        return <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"icon\" href=\"icon/is/here.ico\" />\n            </html>\n        HTML;\n    }\n\n    private static function htmlOptionTwo(): string\n    {\n        return <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"icon\" href=\"/icon/is/here.ico\" />\n            </html>\n        HTML;\n    }\n\n    private static function htmlOptionThree(): string\n    {\n        return <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"shortcut icon\" href=\"/icon/is/here.ico\" />\n            </html>\n        HTML;\n    }\n\n    private static function htmlOptionFour(): string\n    {\n        return <<<'HTML'\n            <html lang=\"en\">\n                <link rel=\"icon\" type=\"image/png\" href=\"https://example.com/favicon/favicon-32x32.png\"/><link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"https://example.com/favicon/apple-icon-57x57.png\"/><link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"https://example.com/favicon/apple-icon-60x60.png\"/><link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"https://example.com/favicon/apple-icon-72x72.png\"/><link rel=\"apple-touch-icon\" sizes=\"76x76\" href=\"https://example.com/favicon/apple-icon-72x72.png\"/><link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"https://example.com/favicon/apple-icon-76x76.png\"/><link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"https://example.com/favicon/apple-icon-120x120.png\"/><link rel=\"apple-touch-icon\" sizes=\"144x144\" href=\"https://example.com/favicon/apple-icon-144x144.png\"/><link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"https://example.com/favicon/apple-icon-152x152.png\"/><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"https://example.com/favicon/apple-icon-180x180.png\"/><link rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"https://example.com/favicon/android-icon-192x192.png\"/>\n            </html>\n        HTML;\n    }\n\n    private static function htmlOptionFive(): string\n    {\n        return <<<'HTML'\n            <html lang=\"en\">\n                <link href=\"/icon/is/here.ico\" rel=\"shortcut icon\" />\n            </html>\n        HTML;\n    }\n\n    private static function htmlOptionSix(): string\n    {\n        return <<<'HTML'\n            <head> <title>Title here</title> <meta name=\"description\" content=\"Meta description here\"> <meta charset=\"utf-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <link rel=\"alternate\" href=\"https://www.example.lv\" hreflang=\"lv\"> <link rel=\"alternate\" href=\"https://www.example.lt/\" hreflang=\"lt\"> <link rel=\"alternate\" href=\"https://www.example.ee/\" hreflang=\"ee\"> <link rel=\"alternate\" href=\"https://www.example.ru/\" hreflang=\"ru\"> <link rel=\"alternate\" href=\"https://www.example.com/en/\" hreflang=\"en\"> <link rel=\"alternate\" href=\"https://www.example.com/default\" hreflang=\"x-default\"> <meta name=\"theme-color\" content=\"#FFFFFF\"> <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/images/apple-icon-180x180.png\"> <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/images/favicon.ico\"> <link rel=\"stylesheet\" href=\"/css/app.css?id=123\"> <script src=\"/vendor/livewire/livewire.js?id=456\" data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" ></script><script data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" >\n        HTML;\n    }\n\n    private static function htmlOptionSeven(): string\n    {\n        return <<<'HTML'\n            <head>\n                <title>Title here</title>\n                <meta name=\"description\" content=\"Meta description here\">\n                <meta charset=\"utf-8\">\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n                <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n                <link rel=\"alternate\" href=\"https://www.example.lv\" hreflang=\"lv\">\n                <link rel=\"alternate\" href=\"https://www.example.lt/\" hreflang=\"lt\">\n                <link rel=\"alternate\" href=\"https://www.example.ee/\" hreflang=\"ee\">\n                <link rel=\"alternate\" href=\"https://www.example.ru/\" hreflang=\"ru\">\n                <link rel=\"alternate\" href=\"https://www.example.com/en/\" hreflang=\"en\">\n                <link rel=\"alternate\" href=\"https://www.example.com/default\" hreflang=\"x-default\">\n                <meta name=\"theme-color\" content=\"#FFFFFF\">\n                <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/images/apple-icon-180x180.png\">\n                <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/images/favicon.ico\">\n                <link rel=\"stylesheet\" href=\"/css/app.css?id=123\">\n                <script src=\"/vendor/livewire/livewire.js?id=456\" data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" ></script>\n                <script data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" ></script>\n            </head>\n        HTML;\n    }\n\n    private static function htmlOptionEight(): string\n    {\n        return <<<'HTML'\n            <head> <title>Title here</title> <meta name=\"description\" content=\"Meta description here\"> <meta charset=\"utf-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <link rel=\"alternate\" href=\"https://www.example.lv\" hreflang=\"lv\"> <link rel=\"alternate\" href=\"https://www.example.lt/\" hreflang=\"lt\"> <link rel=\"alternate\" href=\"https://www.example.ee/\" hreflang=\"ee\"> <link rel=\"alternate\" href=\"https://www.example.ru/\" hreflang=\"ru\"> <link rel=\"alternate\" href=\"https://www.example.com/en/\" hreflang=\"en\"> <link rel=\"alternate\" href=\"https://www.example.com/default\" hreflang=\"x-default\"> <meta name=\"theme-color\" content=\"#FFFFFF\"> <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/images/apple-icon-180x180.png\"> <link rel=\"icon\" type=\"image/x-icon\" href=\"/images/favicon.ico\"> <link rel=\"stylesheet\" href=\"/css/app.css?id=123\"> <script src=\"/vendor/livewire/livewire.js?id=456\" data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" ></script><script data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" >\n        HTML;\n    }\n\n    private static function htmlOptionNine(): string\n    {\n        return <<<'HTML'\n            <head>\n                <title>Title here</title>\n                <meta name=\"description\" content=\"Meta description here\">\n                <meta charset=\"utf-8\">\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n                <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n                <link rel=\"alternate\" href=\"https://www.example.lv\" hreflang=\"lv\">\n                <link rel=\"alternate\" href=\"https://www.example.lt/\" hreflang=\"lt\">\n                <link rel=\"alternate\" href=\"https://www.example.ee/\" hreflang=\"ee\">\n                <link rel=\"alternate\" href=\"https://www.example.ru/\" hreflang=\"ru\">\n                <link rel=\"alternate\" href=\"https://www.example.com/en/\" hreflang=\"en\">\n                <link rel=\"alternate\" href=\"https://www.example.com/default\" hreflang=\"x-default\">\n                <meta name=\"theme-color\" content=\"#FFFFFF\">\n                <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/images/apple-icon-180x180.png\">\n                <link rel=\"icon\" type=\"image/x-icon\" href=\"/images/favicon.ico\">\n                <link rel=\"stylesheet\" href=\"/css/app.css?id=123\">\n                <script src=\"/vendor/livewire/livewire.js?id=456\" data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" ></script>\n                <script data-turbo-eval=\"false\" data-turbolinks-eval=\"false\" ></script>\n            </head>\n        HTML;\n    }\n\n    private static function htmlOptionTen(): string\n    {\n        return <<<'HTML'\n            <head>\n                <title>Test Title</title>\n                <meta content='IE=edge' http-equiv='X-UA-Compatible'>\n                <meta content='telephone=no' name='format-detection'>\n                <meta content='width=device-width, initial-scale=1, maximum-scale=1' name='viewport'>\n                <link href='https://www.example.com/favicon123.png' rel='apple-touch-icon'>\n                <link href='https://www.example.com/favicon123.ico' rel='shortcut icon' type='image/x-icon'>\n            </head>\n        HTML;\n    }\n\n    private static function htmlOptionEleven(): string\n    {\n        return <<<'HTML'\n            <head>\n                <meta charset=\"utf-8\">\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n                <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"/apple-icon-57x57.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"/apple-icon-60x60.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"/apple-icon-72x72.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"76x76\" href=\"/apple-icon-76x76.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"/apple-icon-114x114.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"/apple-icon-120x120.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"144x144\" href=\"/apple-icon-144x144.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"/apple-icon-152x152.png\">\n                <link rel=\"apple-touch-icon\" sizes=\"200x200\" href=\"/apple-icon-200x200.png\">\n                <link rel=\"icon\" type=\"image/png\" sizes=\"192x192\"  href=\"/android-icon-192x192.png\">\n                <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n                <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=\"/favicon-96x96.png\">\n                <link rel=\"manifest\" href=\"/manifest.json\">\n                <meta name=\"msapplication-TileColor\" content=\"#ffffff\">\n                <meta name=\"msapplication-TileImage\" content=\"/ms-icon-144x144.png\">\n                <meta name=\"theme-color\" content=\"#ffffff\">\n                <title>Dummy title</title>\n            </head>\n        HTML;\n    }\n\n    private static function htmlOptionTwelve(): string\n    {\n        return <<<'HTML'\n            <head>\n                <meta charset=\"utf-8\">\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n                <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=/apple-icon-57x57.png>\n                <link rel=apple-touch-icon sizes=60x60 href=/apple-icon-60x60.png>\n                <link rel=\"apple-touch-icon\" sizes=\"72x72\" href=/apple-icon-72x72.png>\n                <link rel=\"apple-touch-icon\" sizes=\"76x76\" href=/apple-icon-76x76.png>\n                <link rel=\"apple-touch-icon\" sizes=\"114x114\" href=/apple-icon-114x114.png>\n                <link rel=\"apple-touch-icon\" sizes=\"120x120\" href=/apple-icon-120x120.png>\n                <link rel=\"apple-touch-icon\" sizes=\"144x144\" href=/apple-icon-144x144.png>\n                <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=/apple-icon-152x152.png>\n                <link rel=\"apple-touch-icon\" sizes=\"200x200\" href=/apple-icon-200x200.png>\n                <link rel=\"icon\" type=\"image/png\" sizes=\"192x192\"  href=/android-icon-192x192.png>\n                <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=/favicon-32x32.png>\n                <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=/favicon-96x96.png>\n                <link rel=\"manifest\" href=\"/manifest.json\">\n                <meta name=\"msapplication-TileColor\" content=\"#ffffff\">\n                <meta name=\"msapplication-TileImage\" content=\"/ms-icon-144x144.png\">\n                <meta name=\"theme-color\" content=\"#ffffff\">\n                <title>Dummy title</title>\n            </head>\n        HTML;\n    }\n\n    private static function htmlOptionThirteen(): string\n    {\n        return <<<'HTML'\n            <head>\n                <meta charset=\"utf-8\" />\n                <title>Dummy title</title>\n                <meta name=\"title\" content=\"Dummy title\">\n                <meta name=\"description\" content=\"Dummy title\">\n                <link rel=\"icon\" type=\"image/x-icon\">\n                <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=/favicon-96x96.png>\n            </head>\n        HTML;\n    }\n}\n"
  },
  {
    "path": "tests/Feature/Drivers/UnavatarDriverTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\UnavatarDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconNotFoundException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidUrlException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\NullDriver;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\TestCase;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass UnavatarDriverTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /**\n     * @test\n     *\n     * @testWith [\"https\"]\n     *           [\"http\"]\n     */\n    public function favicon_can_be_fetched_from_driver(string $protocol): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new UnavatarDriver())->fetch($protocol.'://example.com');\n\n        self::assertSame('https://unavatar.io/example.com?fallback=false', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void\n    {\n        Cache::put(\n            'favicon-fetcher.example.com',\n            [\n                'favicon_url' => 'url-goes-here',\n                'icon_size' => null,\n                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n            ],\n            now()->addHour()\n        );\n\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new UnavatarDriver())->fetch('https://example.com');\n\n        self::assertSame('url-goes-here', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void\n    {\n        Cache::put(\n            'favicon-fetcher.https://example.com',\n            'url-goes-here',\n            now()->addHour()\n        );\n\n        Http::fake([\n            'https://unavatar.io/example.com' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new UnavatarDriver())->useCache(false)->fetch('https://example.com');\n\n        self::assertSame('https://unavatar.io/example.com?fallback=false', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new UnavatarDriver())->useCache(true)->fetch('https://example.com');\n\n        self::assertNull($favicon);\n    }\n\n    /** @test */\n    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new CustomDriver());\n\n        $favicon = (new UnavatarDriver())\n            ->withFallback('custom-driver')\n            ->useCache(true)\n            ->fetch('https://example.com');\n\n        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new UnavatarDriver())\n                ->throw()\n                ->useCache(true)\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method(): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new UnavatarDriver())\n            ->useCache(true)\n            ->fetchOr('https://example.com', 'fallback-to-this');\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = (new UnavatarDriver())\n            ->fetchOr('https://example.com', function () {\n                return 'fallback-to-this';\n            });\n\n        self::assertSame('fallback-to-this', $favicon);\n    }\n\n    /** @test */\n    public function exception_can_be_thrown_after_attempting_a_fallback(): void\n    {\n        Http::fake([\n            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        FetcherManager::extend('custom-driver', new NullDriver());\n\n        $exception = null;\n\n        try {\n            (new UnavatarDriver())\n                ->throw()\n                ->withFallback('custom-driver')\n                ->fetch('https://example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(FaviconNotFoundException::class, $exception);\n        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());\n\n        self::assertTrue(NullDriver::$flag);\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_url_is_invalid(): void\n    {\n        Http::fake([\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $exception = null;\n\n        try {\n            (new UnavatarDriver())->fetch('example.com');\n        } catch (\\Exception $e) {\n            $exception = $e;\n        }\n\n        self::assertInstanceOf(InvalidUrlException::class, $exception);\n        self::assertSame('example.com is not a valid URL', $exception->getMessage());\n    }\n}\n"
  },
  {
    "path": "tests/Feature/FaviconTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature;\n\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidIconSizeException;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\InvalidIconTypeException;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\nuse Carbon\\CarbonInterface;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Illuminate\\Support\\Str;\nuse Mockery;\n\nclass FaviconTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /** @test */\n    public function favicon_url_can_be_returned(): void\n    {\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        );\n\n        self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());\n    }\n\n    /** @test */\n    public function favicon_contents_can_be_returned(): void\n    {\n        Http::fake([\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        );\n\n        self::assertSame('favicon contents here', $favicon->content());\n    }\n\n    /** @test */\n    public function url_can_be_returned(): void\n    {\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        );\n\n        self::assertSame('https://example.com', $favicon->getUrl());\n    }\n\n    /** @test */\n    public function retrieved_from_cache_value_can_be_returned_if_the_favicon_was_retrieved_from_the_cache(): void\n    {\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n            retrievedFromCache: true,\n        );\n\n        self::assertTrue($favicon->retrievedFromCache());\n    }\n\n    /** @test */\n    public function retrieved_from_cache_value_can_be_returned_if_the_favicon_was_not_retrieved_from_the_cache(): void\n    {\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        );\n\n        self::assertFalse($favicon->retrievedFromCache());\n    }\n\n    /** @test */\n    public function favicon_can_be_cached_if_it_is_not_already_cached(): void\n    {\n        Carbon::setTestNow(now());\n\n        $expectedTtl = now()->addMinute();\n\n        Cache::shouldReceive('put')\n            ->withArgs([\n                'favicon-fetcher.example.com',\n                [\n                    'favicon_url' => 'https://example.com/favicon.ico',\n                    'icon_size' => null,\n                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n                ],\n                Mockery::on(fn (CarbonInterface $ttl): bool => $ttl->eq($expectedTtl)),\n            ])\n            ->once();\n\n        (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))->cache($expectedTtl);\n    }\n\n    /** @test */\n    public function favicon_cannot_be_cached_if_it_is_already_cached(): void\n    {\n        Carbon::setTestNow(now());\n\n        $expectedTtl = now()->addMinute();\n\n        Cache::shouldReceive('put')->never();\n\n        (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n            retrievedFromCache: true,\n        ))->cache($expectedTtl);\n    }\n\n    /** @test */\n    public function favicon_can_be_cached_if_it_is_already_cached_and_the_force_flag_is_passed(): void\n    {\n        Carbon::setTestNow(now());\n\n        $expectedTtl = now()->addMinute();\n\n        Cache::shouldReceive('put')\n            ->withArgs([\n                'favicon-fetcher.example.com',\n                [\n                    'favicon_url' => 'https://example.com/favicon.ico',\n                    'icon_size' => null,\n                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,\n                ],\n                Mockery::on(fn (CarbonInterface $ttl): bool => $ttl->eq($expectedTtl)),\n            ])\n            ->once();\n\n        (new Favicon(\n            'https://example.com',\n            'https://example.com/favicon.ico',\n        ))->cache(now()->addMinute(), true);\n    }\n\n    /** @test */\n    public function favicon_contents_be_stored(): void\n    {\n        Storage::fake();\n\n        Http::fake([\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        );\n\n        $path = $favicon->store('favicons');\n\n        self::assertSame('favicon contents here', Storage::get($path));\n    }\n\n    /** @test */\n    public function favicon_contents_be_stored_if_the_favicon_url_does_not_have_an_image_extension(): void\n    {\n        Storage::fake();\n\n        Http::fake([\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            'https://example.com/favicon.com' => Http::response(body: 'favicon contents here', headers: ['content-type' => 'image/png']),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        $favicon = new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.com',\n        );\n\n        $path = $favicon->store('favicons');\n\n        self::assertSame('favicon contents here', Storage::get($path));\n        self::assertTrue(Str::of($path)->endsWith('.png'));\n    }\n\n    /** @test */\n    public function favicon_contents_can_be_stored_with_a_custom_file_name(): void\n    {\n        Storage::fake();\n\n        Http::fake([\n            'https://example.com/favicon.ico' => Http::response('favicon contents here'),\n            '*' => Http::response('should not hit here'),\n        ]);\n\n        (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))->storeAs('favicons', 'fetched');\n\n        self::assertSame('favicon contents here', Storage::get('favicons/fetched.ico'));\n    }\n\n    public function icon_type_defaults_to_unknown_if_not_explicitly_set(): void\n    {\n        $iconType = (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))->getIconType();\n\n        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $iconType);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider iconTypeProvider\n     */\n    public function icon_type_can_be_set_and_returned(string $expectedIconType): void\n    {\n        $iconType = (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))\n            ->setIconType($expectedIconType)\n            ->getIconType();\n\n        self::assertSame($expectedIconType, $iconType);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider iconSizeProvider\n     */\n    public function icon_size_can_be_set_and_returned(?int $expectedIconSize): void\n    {\n        $iconSize = (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))\n            ->setIconSize($expectedIconSize)\n            ->getIconSize();\n\n        self::assertSame($expectedIconSize, $iconSize);\n    }\n\n    /** @test */\n    public function exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_type(): void\n    {\n        $this->expectException(InvalidIconTypeException::class);\n        $this->expectExceptionMessage('The type [INVALID] is not a valid favicon type.');\n\n        (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))->setIconType('INVALID');\n    }\n\n    /** @test */\n    public function exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_size(): void\n    {\n        $this->expectException(InvalidIconSizeException::class);\n        $this->expectExceptionMessage('The size [-1] is not a valid favicon size.');\n\n        (new Favicon(\n            url: 'https://example.com',\n            faviconUrl: 'https://example.com/favicon.ico',\n        ))->setIconSize(-1);\n    }\n\n    public static function iconTypeProvider(): array\n    {\n        return [\n            [Favicon::TYPE_ICON],\n            [Favicon::TYPE_SHORTCUT_ICON],\n            [Favicon::TYPE_APPLE_TOUCH_ICON],\n            [Favicon::TYPE_ICON_UNKNOWN],\n        ];\n    }\n\n    public static function iconSizeProvider(): array\n    {\n        return [\n            [null],\n            [16],\n            [190],\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Feature/FetcherManagerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature;\n\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\DuckDuckGoDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\FaviconGrabberDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\FaviconKitDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\GoogleSharedStuffDriver;\nuse AshAllenDesign\\FaviconFetcher\\Drivers\\HttpDriver;\nuse AshAllenDesign\\FaviconFetcher\\Exceptions\\FaviconFetcherException;\nuse AshAllenDesign\\FaviconFetcher\\Facades\\Favicon;\nuse AshAllenDesign\\FaviconFetcher\\FetcherManager;\nuse AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data\\CustomDriver;\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Mockery;\n\nclass FetcherManagerTest extends TestCase\n{\n    use LazilyRefreshDatabase;\n\n    /** @test */\n    public function default_driver_can_be_returned(): void\n    {\n        config(['favicon-fetcher.default' => 'http']);\n\n        self::assertInstanceOf(HttpDriver::class, FetcherManager::driver());\n    }\n\n    /** @test */\n    public function http_driver_can_be_returned(): void\n    {\n        self::assertInstanceOf(HttpDriver::class, FetcherManager::driver('http'));\n    }\n\n    /** @test */\n    public function google_shared_stuff_driver_can_be_returned(): void\n    {\n        self::assertInstanceOf(GoogleSharedStuffDriver::class, FetcherManager::driver('google-shared-stuff'));\n    }\n\n    /** @test */\n    public function favicon_kit_driver_can_be_returned(): void\n    {\n        self::assertInstanceOf(FaviconKitDriver::class, FetcherManager::driver('favicon-kit'));\n    }\n\n    /** @test */\n    public function favicon_grabber_driver_can_be_returned(): void\n    {\n        self::assertInstanceOf(FaviconGrabberDriver::class, FetcherManager::driver('favicon-grabber'));\n    }\n\n    /** @test */\n    public function duck_duck_go_driver_can_be_returned(): void\n    {\n        self::assertInstanceOf(DuckDuckGoDriver::class, FetcherManager::driver('duck-duck-go'));\n    }\n\n    /** @test */\n    public function custom_driver_can_be_returned(): void\n    {\n        FetcherManager::extend('custom-driver', new CustomDriver());\n        self::assertInstanceOf(CustomDriver::class, FetcherManager::driver('custom-driver'));\n    }\n\n    /** @test */\n    public function exception_is_thrown_if_the_driver_is_invalid(): void\n    {\n        $this->expectException(FaviconFetcherException::class);\n        $this->expectExceptionMessage('invalid is not a valid driver');\n\n        FetcherManager::driver('invalid');\n    }\n\n    /** @test */\n    public function method_calls_to_the_manager_are_forwarded_to_the_driver(): void\n    {\n        $mock = tap(\n            Mockery::mock(CustomDriver::class),\n            function (Mockery\\MockInterface $mock): void {\n                $mock->shouldReceive('fetch')\n                    ->once()\n                    ->withArgs(['https://example.com']);\n            }\n        );\n\n        FetcherManager::extend('custom-driver', $mock);\n\n        config(['favicon-fetcher.default' => 'custom-driver']);\n\n        (new FetcherManager())->fetch('https://example.com');\n    }\n\n    /** @test */\n    public function method_calls_to_the_manager_are_forwarded_to_the_driver_using_the_facade(): void\n    {\n        $mock = tap(\n            Mockery::mock(CustomDriver::class),\n            function (Mockery\\MockInterface $mock): void {\n                $mock->shouldReceive('fetch')\n                    ->once()\n                    ->withArgs(['https://example.com']);\n            }\n        );\n\n        FetcherManager::extend('custom-driver', $mock);\n\n        config(['favicon-fetcher.default' => 'custom-driver']);\n\n        Favicon::fetch('https://example.com');\n    }\n\n    /** @test */\n    public function driver_can_be_returned_using_the_facade(): void\n    {\n        self::assertInstanceOf(FaviconKitDriver::class, Favicon::driver('favicon-kit'));\n    }\n}\n"
  },
  {
    "path": "tests/Feature/TestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature;\n\nuse AshAllenDesign\\FaviconFetcher\\FaviconFetcherProvider;\nuse Orchestra\\Testbench\\TestCase as OrchestraTestCase;\n\nabstract class TestCase extends OrchestraTestCase\n{\n    /**\n     * Load package service provider.\n     *\n     * @param  $app\n     * @return array\n     */\n    protected function getPackageProviders($app)\n    {\n        return [FaviconFetcherProvider::class];\n    }\n}\n"
  },
  {
    "path": "tests/Feature/_data/CustomDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\n\nclass CustomDriver implements Fetcher\n{\n    public function fetch(string $url): ?Favicon\n    {\n        return new Favicon(\n            url: 'url-from-default',\n            faviconUrl: 'favicon-from-default',\n            fromDriver: $this,\n        );\n    }\n\n    public function fetchOr(string $url, mixed $default): mixed\n    {\n        return 'default';\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        // Implement this method if needed for testing.\n    }\n\n    public function fetchAllOr(string $url, mixed $default): mixed\n    {\n        // Implement this method if needed for testing.\n    }\n}\n"
  },
  {
    "path": "tests/Feature/_data/NullDriver.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection;\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\Fetcher;\nuse AshAllenDesign\\FaviconFetcher\\Favicon;\n\nclass NullDriver implements Fetcher\n{\n    public static bool $flag = false;\n\n    public function fetch(string $url): ?Favicon\n    {\n        static::$flag = true;\n\n        return null;\n    }\n\n    public function fetchOr(string $url, mixed $default): mixed\n    {\n        return 'default';\n    }\n\n    public function fetchAll(string $url): FaviconCollection\n    {\n        // Implement this method if needed for testing.\n    }\n\n    public function fetchAllOr(string $url, mixed $default): mixed\n    {\n        // Implement this method if needed for testing.\n    }\n}\n"
  }
]