Full Code of ash-jc-allen/favicon-fetcher for AI

master 75393d3c2227 cached
52 files
187.3 KB
47.0k tokens
250 symbols
1 requests
Download .txt
Showing preview only (202K chars total). Download the full file or copy to clipboard to get everything.
Repository: ash-jc-allen/favicon-fetcher
Branch: master
Commit: 75393d3c2227
Files: 52
Total size: 187.3 KB

Directory structure:
gitextract_e3mdaezn/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci-phpstan.yml
│       └── ci-tests.yml
├── .gitignore
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer.json
├── config/
│   └── favicon-fetcher.php
├── phpstan.neon
├── phpunit.xml
├── src/
│   ├── Collections/
│   │   └── FaviconCollection.php
│   ├── Concerns/
│   │   ├── BuildsCacheKeys.php
│   │   ├── HasDefaultFunctionality.php
│   │   ├── MakesHttpRequests.php
│   │   └── ValidatesUrls.php
│   ├── Contracts/
│   │   └── Fetcher.php
│   ├── Drivers/
│   │   ├── DuckDuckGoDriver.php
│   │   ├── FaviconGrabberDriver.php
│   │   ├── FaviconKitDriver.php
│   │   ├── GoogleSharedStuffDriver.php
│   │   ├── HttpDriver.php
│   │   └── UnavatarDriver.php
│   ├── Exceptions/
│   │   ├── ConnectionException.php
│   │   ├── FaviconFetcherException.php
│   │   ├── FaviconNotFoundException.php
│   │   ├── FeatureNotSupportedException.php
│   │   ├── InvalidIconSizeException.php
│   │   ├── InvalidIconTypeException.php
│   │   └── InvalidUrlException.php
│   ├── Facades/
│   │   └── Favicon.php
│   ├── Favicon.php
│   ├── FaviconFetcherProvider.php
│   └── FetcherManager.php
└── tests/
    └── Feature/
        ├── Collections/
        │   └── FaviconCollectionTest.php
        ├── Concerns/
        │   └── MakesHttpRequests/
        │       ├── HttpClientTest.php
        │       └── WithRequestExceptionHandlingTest.php
        ├── Drivers/
        │   ├── DuckDuckGoDriverTest.php
        │   ├── FaviconGrabberDriverTest.php
        │   ├── FaviconKitDriverTest.php
        │   ├── GoogleSharedStuffDriverTest.php
        │   ├── HttpDriverTest.php
        │   └── UnavatarDriverTest.php
        ├── FaviconTest.php
        ├── FetcherManagerTest.php
        ├── TestCase.php
        └── _data/
            ├── CustomDriver.php
            └── NullDriver.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Ignore all test and documentation with "export-ignore".
/.github           export-ignore
/docs              export-ignore
/tests             export-ignore
/.gitattributes    export-ignore
/.gitignore        export-ignore
/.styleci.yml      export-ignore
/phpstan.neon      export-ignore
/phpunit.xml       export-ignore
/CHANGELOG.md      export-ignore
/UPGRADE.md        export-ignore


================================================
FILE: .github/FUNDING.yml
================================================
github: ash-jc-allen


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: composer
  directory: "/"
  schedule:
    interval: daily
  open-pull-requests-limit: 10


================================================
FILE: .github/workflows/ci-phpstan.yml
================================================
name: run-phpstan

on:
  pull_request:

jobs:
  run-tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: [8.1, 8.2, 8.3, 8.4, 8.5]
        laravel: [9.*, 10.*, 11.*, 12.*, 13.*]
        include:
          - laravel: 13.*
            testbench: 11.*
          - laravel: 12.*
            testbench: 10.*
          - laravel: 11.*
            testbench: 9.*
          - laravel: 10.*
            testbench: 8.*
          - laravel: 9.*
            testbench: 7.*
        exclude:
          - php: 8.1
            laravel: 11.*
          - php: 8.1
            laravel: 12.*
          - php: 8.1
            laravel: 13.*
          - php: 8.2
            laravel: 13.*

    name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}

    steps:
      - name: Update apt
        run: sudo apt-get update --fix-missing

      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none

      - name: Setup Problem Matches
        run: |
          echo "::add-matcher::${{ runner.tool_cache }}/php.json"
          echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

      - name: Install dependencies
        run: |
          composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
          composer update --prefer-dist --no-interaction --no-suggest
      - name: Run Larastan
        run: composer larastan


================================================
FILE: .github/workflows/ci-tests.yml
================================================
name: run-tests

on:
  pull_request:

jobs:
  run-tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: [8.1, 8.2, 8.3, 8.4, 8.5]
        laravel: [9.*, 10.*, 11.*, 12.*, 13.*]
        include:
          - laravel: 13.*
            testbench: 11.*
          - laravel: 12.*
            testbench: 10.*
          - laravel: 11.*
            testbench: 9.*
          - laravel: 10.*
            testbench: 8.*
          - laravel: 9.*
            testbench: 7.*
        exclude:
          - php: 8.1
            laravel: 11.*
          - php: 8.1
            laravel: 12.*
          - php: 8.1
            laravel: 13.*
          - php: 8.2
            laravel: 13.*

    name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}

    steps:
      - name: Update apt
        run: sudo apt-get update --fix-missing

      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none

      - name: Setup Problem Matches
        run: |
          echo "::add-matcher::${{ runner.tool_cache }}/php.json"
          echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

      - name: Install dependencies
        run: |
          composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
          composer update --prefer-dist --no-interaction --no-suggest
      - name: Execute tests
        run: composer test


================================================
FILE: .gitignore
================================================
.idea/
vendor/
composer.lock
.phpunit.result.cache

================================================
FILE: .styleci.yml
================================================
preset: laravel

================================================
FILE: CHANGELOG.md
================================================
# Changelog

**v3.11.0 (released 2026-03-13):**

- Added support for Laravel 13. ((#96)[https://github.com/ash-jc-allen/favicon-fetcher/pull/96])
- Run CI workflows with PHP 8.5. ([#95](https://github.com/ash-jc-allen/favicon-fetcher/pull/95))

**v3.10.0 (released 2025-10-17):**

- Fixed typo. ([#93](https://github.com/ash-jc-allen/favicon-fetcher/pull/93))
- Added `.gitattributes` file. ([#94](https://github.com/ash-jc-allen/favicon-fetcher/pull/94))

**v3.9.0 (released 2025-07-25):**

- Added driver for the DuckDuckGo icons API. ([#88](https://github.com/ash-jc-allen/favicon-fetcher/pull/88))

**v3.8.0 (released 2025-02-24):**

- Added support for Laravel 12. ([#82](https://github.com/ash-jc-allen/favicon-fetcher/pull/82))
- Added support for PHPUnit 11. ([#83](https://github.com/ash-jc-allen/favicon-fetcher/pull/83))
- Migrated from `nunomaduro/larastan` to `larastan/larastan`. ([#84](https://github.com/ash-jc-allen/favicon-fetcher/pull/84))
- Added support for Larastan 3. ([#84](https://github.com/ash-jc-allen/favicon-fetcher/pull/84))

**v3.7.0 (released 2024-11-30):**

- Added explicit nullable types to support PHP 8.4. ([#80](https://github.com/ash-jc-allen/favicon-fetcher/pull/80))

**v3.6.0 (released 2024-07-08):**

- Added support for `symfony/dom-crawler` v7.0. ([#79](https://github.com/ash-jc-allen/favicon-fetcher/pull/79))

**v3.5.0 (released 2024-06-14):**

- Added a new `verify_tls` config option to disable TLS certificate verification. ([#78](https://github.com/ash-jc-allen/favicon-fetcher/pull/78))

**v3.4.1 (released 2024-04-30):**

- 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))

**v3.4.0 (released 2024-03-19):**

- Added support for `nesbot/carbon 3.0`. ([#76](https://github.com/ash-jc-allen/favicon-fetcher/pull/76))

**v3.3.0 (released 2024-03-12):**

- Added support for Laravel 11. ([#75](https://github.com/ash-jc-allen/favicon-fetcher/pull/75))

**v3.2.0 (released 2024-01-29):**

- Added a `largestByFileSize` method to the `FaviconCollection`. [#73](https://github.com/ash-jc-allen/favicon-fetcher/pull/73)

**v3.1.0 (released 2023-11-07):**

- Added `user_agent` config field to configure HTTP `User-Agent` request header. ([#70](https://github.com/ash-jc-allen/favicon-fetcher/pull/70))
- Run CI tests using PHP 8.3 ([#69](https://github.com/ash-jc-allen/favicon-fetcher/pull/69))

**v3.0.0 (released 2023-09-04):**

- Added `connect_timeout` and `timeout` config fields. ([#67](https://github.com/ash-jc-allen/favicon-fetcher/pull/67))
- Use `symfony/dom-crawler` in the `HttpDriver` to parse the HTML. ([#56](https://github.com/ash-jc-allen/favicon-fetcher/pull/56))
- Updated all files to use strict types for improved type safety. ([#62](https://github.com/ash-jc-allen/favicon-fetcher/pull/62))
- Throw package-specific exceptions instead of vendor exceptions. ([#67](https://github.com/ash-jc-allen/favicon-fetcher/pull/67))
- 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))
- 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))
- 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))
- Dropped support for PHP 8.0. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))
- Dropped support for Laravel 8. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))
- Dropped support for PHPUnit 8.* and Larastan 1.*. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))

**v2.0.0 (released 2023-03-23):**
- Added driver for the [Favicon Grabber API](https://favicongrabber.com/). ([#24](https://github.com/ash-jc-allen/favicon-fetcher/pull/24))
- 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))
- Added `fetchAll` method to the `AshAllenDesign\FaviconFetcher\Contracts\Fetcher` interface. ([#29](https://github.com/ash-jc-allen/favicon-fetcher/pull/29))
- 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))
- 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))
- Changed the values that are used when caching a favicon. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))
- Removed the `makeFromCache` method from the `Favicon` class. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))

**v1.3.0 (released 2023-01-12):**
- Added support for Laravel 10. (([#22](https://github.com/ash-jc-allen/favicon-fetcher/pull/22)))

**v1.2.1 (released 2022-11-08):**
- 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))

**v1.2.0 (released 2022-10-17):**
- Added support for PHP 8.2. ([#21](https://github.com/ash-jc-allen/favicon-fetcher/pull/21))

**v1.1.3 (released 2022-09-03):**
- Removed an incorrect mime type from the file extension detection. ([#19](https://github.com/ash-jc-allen/favicon-fetcher/pull/19))

**v1.1.2 (released 2022-07-23):**
- 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))

**v1.1.1 (released 2022-05-10):**
- 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))

**v1.1.0 (released 2022-04-27):**
- Added driver for [Unavatar](https://unavatar). ([#8](https://github.com/ash-jc-allen/favicon-fetcher/pull/8))

**v1.0.0 (released 2022-04-26):**
- Initial release.


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2022 Ashley Allen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
<img src="/docs/logo.png" alt="Favicon Fetcher" width="600">
</p>

<p align="center">
<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>
<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>
<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>
<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>
</p>

## Table of Contents

- [Overview](#overview)
- [Installation](#installation)
    * [Requirements](#requirements)
    * [Install the Package](#install-the-package)
    * [Publish the Config](#publish-the-config)
- [Usage](#usage)
    * [Fetching Favicons](#fetching-favicons)
        + [Using the `fetch` Method](#using-the-fetch-method)
        + [Using the `fetchOr` Method](#using-the-fetchor-method)
        + [Using the `fetchAll` Method](#using-the-fetchall-method)
        + [Using the `fetchAllOr` Method](#using-the-fetchallor-method)
    * [Exceptions](#exceptions)
    * [Drivers](#drivers)
    * [Available Drivers](#available-drivers)
        + [How to Choose a Driver](#how-to-choose-a-driver)
    * [Choosing a Driver](#choosing-a-driver)
        + [Fallback Drivers](#fallback-drivers)
        + [Adding Your Own Driver](#adding-your-own-driver)
    * [HTTP Timeouts](#http-timeouts)
    * [TLS Verification](#tls-verification)
    * [HTTP User Agent](#http-user-agent)
    * [Storing Favicons](#storing-favicons)
        + [Using `store`](#using-store)
        + [Using `storeAs`](#using-storeas)
    * [Caching Favicons](#caching-favicons)
    * [Favicon Types](#favicon-types)
    * [Favicon Sizes](#favicon-sizes)
- [Testing](#testing)
- [Security](#security)
- [Contribution](#contribution)
- [Changelog](#changelog)
- [Upgrading](#upgrading)
- [Credits](#credits)
- [License](#license)

## Overview

A Laravel package that can be used for fetching favicons from websites.

## Installation

### Requirements

The package has been developed and tested to work with the following minimum requirements:

- PHP 8.0
- Laravel 8.0

### Install the Package

You can install the package via Composer:

```bash
composer require ashallendesign/favicon-fetcher
```

### Publish the Config
You can then publish the package's config file by using the following command:

```bash
php artisan vendor:publish --provider="AshAllenDesign\FaviconFetcher\FaviconFetcherProvider"
```

## Usage

### Fetching Favicons

Now that you have the package installed, you can start fetching the favicons from different websites.

#### Using the `fetch` Method

To fetch a favicon from a website, you can use the `fetch` method which will return an instance of `AshAllenDesign\FaviconFetcher\Favicon`:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetch('https://ashallendesign.co.uk');
```

#### Using the `fetchOr` Method

If you'd like to provide a default value to be used if a favicon cannot be found, you can use the `fetchOr` method.

For 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetchOr('https://ashallendesign.co.uk', 'https://example.com/favicon.ico');
```

This 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetchOr('https://ashallendesign.co.uk', function ($url) {
    // Run extra logic here...

    return 'https://example.com/favicon.ico';
});
```

#### Using the `fetchAll` Method

There 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicons = Favicon::fetchAll('https://ashallendesign.co.uk');
```

The `FaviconCollection` class extends the `Illuminate\Support\Collection` class, so you can use all the methods available on the `Collection` class.

It 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$largestFavicon = Favicon::fetchAll('https://ashallendesign.co.uk')->largest();
```

The `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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$largestFavicon = Favicon::fetchAll('https://ashallendesign.co.uk')->largestByFileSize();
```

Note: 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. 

#### Using the `fetchAllOr` Method

If 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.

For 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetchAllOr('https://ashallendesign.co.uk', 'https://example.com/favicon.ico');
```

This 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetchAllOr('https://ashallendesign.co.uk', function ($url) {
    // Run extra logic here...

    return 'https://example.com/favicon.ico';
});
```

### Exceptions

By 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.

To enable exceptions to be thrown, your code could look something like this:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::throw()->fetch('https://ashallendesign.co.uk');
```

If 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.

### Drivers

Favicon Fetcher provides the functionality to use different drivers for retrieving favicons from websites.

### Available Drivers

By default, Favicon Fetcher ships with 5 drivers out-the-box: `http`, `google-shared-stuff`, `favicon-kit`, `unavatar`, `favicon-grabber`, `duck-duck-go`.

The `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.

The `google-shared-stuff` driver fetches favicons using the [Google Shared Stuff](https://google.com) API.

The `favicon-kit` driver fetches favicons using the [Favicon Kit](https://faviconkit.com) API.

The `unavatar` driver fetches favicons using the [Unavatar](https://unavatar.io) API.

The `favicon-grabber` driver fetches favicons using the [Favicon Grabber](https://favicongrabber.com) API.

The `duck-duck-go` driver fetches favicons using the [DuckDuckGo Icons](https://duckduckgo.com) API.

#### How to Choose a Driver

It'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.

However, 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. 

### Choosing a Driver

You 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.

For example, if you wanted to change your default driver to `favicon-kit`, you could update your `favicon-fetcher` config like so:

```php
return [

    // ...
        
    'default' => 'favicon-kit',
            
    // ...

]
```

If 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::driver('google-shared-stuff')->fetch('https://ashallendesign.co.uk');
```

#### Fallback Drivers

There 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.

For 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::withFallback('google-shared-stuff')->fetch('https://ashallendesign.co.uk');
```

#### Adding Your Own Driver

There 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.

First, 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:

```php
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Favicon;

class MyCustomDriver implements Fetcher
{
    public function fetch(string $url): ?Favicon
    {
        // Add logic here that attempts to fetch a favicon...
    }

    public function fetchOr(string $url, mixed $default): mixed
    {
        // Add logic here that attempts to fetch a favicon or return a default...
    }
}
```

After 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.

You can register your custom driver like so:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

Favicon::extend('my-custom-driver', new MyCustomDriver());
```

Now that you've registered your custom driver, you'll be able to use it for fetching favicons like so:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::driver('my-custom-driver')->fetch('https://ashallendesign.co.uk');
```

### HTTP Timeouts

Favicon Fetcher provides the ability for you to set the connection timeout and request timeout for all the drivers.

The 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.

To 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:

```php
return [

    // ...
        
    'connect_timeout' => 5,

    'timeout' => 10,
            
    // ...

]
```

If you'd prefer that no timeout be set, you can set the values to `0`.

Please note that these timeouts are applied to all HTTP requests that Favicon Fetcher makes, regardless of the driver that is being used.

### TLS Verification

Favicon 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.

You can disable the verification by updating the `verify_tls` field in the `favicon-fetcher.php` config file after you've published it.

```php
return [

    // ...
        
    'verify_tls' => false,
            
    // ...

]
```

Or by updating your `.env` file:

```dotenv
FAVICON_FETCHER_VERIFY_TLS=false
```

### HTTP User Agent

You 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.

Favicon 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:

```php
return [

    // ...
        
    'user_agent' => 'My Custom User Agent',
            
    // ...

]
```

The `User-Agent` header will be set on all HTTP requests that Favicon Fetcher makes, regardless of the driver that is being used.

The `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:

```dotenv
FAVICON_FETCHER_USER_AGENT="My Custom User Agent"
```

### Storing Favicons

After 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`.

#### Using `store`

If 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->store('favicons');

// $faviconPath is now equal to: "/favicons/abc-123.ico"
```

If 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->store('favicons', 's3');

// $faviconPath is now equal to: "/favicons/abc-123.ico"
```

#### Using `storeAs`

If 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->storeAs('favicons', 'ashallendesign');

// $faviconPath is now equal to: "/favicons/ashallendesign.ico"
```

If 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->storeAs('favicons', 'ashallendesign', 's3');

// $faviconPath is now equal to: "/favicons/ashallendesign.ico"
```

### Caching Favicons

As 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.

As 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.

To 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetch('https://ashallendesign.co.uk')->cache(now()->addDay());
```

By 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::useCache(false)->fetch('https://ashallendesign.co.uk');
```

The 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:

```php
return [

    // ...
        
    'cache' => [
        'prefix' => 'my-awesome-prefix',
    ]
            
    // ...

]
```

The 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconCollection = Favicon::fetchAll('https://ashallendesign.co.uk')->cache(now()->addDay());
```

### Favicon Types

When 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->getIconType();
```

This 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`.

You 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconCollection = Favicon::fetchAll('https://ashallendesign.co.uk');

$faviconCollection->filter(function ($favicon) {
    return $favicon->getIconType() !== Favicon::TYPE_APPLE_TOUCH_ICON;
});
```

### Favicon Sizes

When 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:

```php
use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$faviconSize = Favicon::fetch('https://ashallendesign.co.uk')->getIconSize();
```

It'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.

## Testing

To run the package's unit tests, run the following command:

``` bash
composer test
```

To run Larastan for the package, run the following command:

```bash
composer larastan
```

## Security

If you find any security related issues, please contact me directly at [mail@ashallendesign.co.uk](mailto:mail@ashallendesign.co.uk) to report it.

## Contribution

If you wish to make any changes or improvements to the package, feel free to make a pull request.

To contribute to this package, please use the following guidelines before submitting your pull request:

- Write tests for any new functions that are added. If you are updating existing code, make sure that the existing tests
  pass and write more if needed.
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standards.
- Make all pull requests to the `master` branch.

## Changelog

Check the [CHANGELOG](CHANGELOG.md) to get more information about the latest changes.

## Upgrading

Check the [UPGRADE](UPGRADE.md) guide to get more information on how to update this library to newer versions.

## Credits

- [Ash Allen](https://ashallendesign.co.uk)
- [Jess Pickup](https://jesspickup.co.uk) (Logo)
- [All Contributors](https://github.com/ash-jc-allen/short-url/graphs/contributors)

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

## Support Me

If 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.

Every sale makes a huge difference to me and allows me to spend more time working on open-source projects and tutorials.

To say a huge thanks, you can use the code **BATTLE20** to get a 20% discount on the book.

[👉 Get Your Copy!](https://battle-ready-laravel.com)

[![Battle Ready Laravel](https://ashallendesign.co.uk/images/custom/sponsors/battle-ready-laravel-horizontal-banner.png)](https://battle-ready-laravel.com)


================================================
FILE: UPGRADE.md
================================================
# Upgrade Guide

## Contents

- [Upgrading from 2.* to 3.0.0](#upgrading-from-2-to-300)
- [Upgrading from 1.* to 2.0.0](#upgrading-from-1-to-200)

## Upgrading from 2.* to 3.0.0

### Exceptions

Previously, 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.

This will be thrown in the following situations:

- If the URL has a valid structure but the site doesn't exist.
- If the HTTP client exceeds the connection timeout.
- If the HTTP client exceeds the request timeout.

## Upgrading from 1.* to 2.0.0

### Method Visibility Changes

The 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`.

### Added `fetchAll` and `fetchAllOr` Methods to `Fetcher` Interface

The `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.

The signatures for the new methods are:

```php
public function fetchAll(string $url): FaviconCollection;
```

```php
public function fetchAllOr(string $url, mixed $default): mixed;
```

### Removed `makeFromCache` Method from `Favicon` Class

The `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. 

If you were making use of this method anywhere, you'll need to remove it from your code.

### Caching Changes

Previously, 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.

This means that instead of a string being stored in the cache, an array is now stored instead.

The 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.

In a future release (likely v3.0.0), the backwards-compatible support will be removed so that only arrays can be read from the cache.


================================================
FILE: composer.json
================================================
{
  "name": "ashallendesign/favicon-fetcher",
  "description": "A Laravel package for fetching website's favicons.",
  "type": "library",
  "homepage": "https://github.com/ash-jc-allen/favicon-fetcher",
  "license": "MIT",
  "authors": [
    {
      "name": "Ash Allen",
      "email": "mail@ashallendesign.co.uk"
    }
  ],
  "keywords": [
    "ashallendesign",
    "favicon-fetcher",
    "favicon",
    "icon"
  ],
  "require": {
    "php": "^8.1",
    "nesbot/carbon": "^2.0|^3.0",
    "illuminate/cache": "^9.0|^10.0|^11.0|^12.0|^13.0",
    "illuminate/filesystem": "^9.0|^10.0|^11.0|^12.0|^13.0",
    "illuminate/http": "^9.0|^10.0|^11.0|^12.0|^13.0",
    "guzzlehttp/guzzle": "^7.4",
    "symfony/dom-crawler": "^6.3 || ^7.0"
  },
  "require-dev": {
    "mockery/mockery": "^1.0",
    "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0",
    "phpunit/phpunit": "^9.0|^10.0|^11.0",
    "larastan/larastan": "^2.0|^3.0"
  },
  "autoload": {
    "psr-4": {
      "AshAllenDesign\\FaviconFetcher\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
        "AshAllenDesign\\FaviconFetcher\\Tests\\": "tests/"
    }
  },
  "extra": {
    "laravel": {
      "providers": [
        "AshAllenDesign\\FaviconFetcher\\FaviconFetcherProvider"
      ]
    }
  },
  "scripts": {
    "test": "vendor/bin/phpunit",
    "larastan": "vendor/bin/phpstan analyse"
  },
  "minimum-stability": "dev",
  "prefer-stable": true
}


================================================
FILE: config/favicon-fetcher.php
================================================
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Driver
    |--------------------------------------------------------------------------
    |
    | The driver that should be used by default for fetching the favicons.
    | By default, the package comes with support for several drivers,
    | but you can define your own if needed.
    |
    | Supported drivers: "http", "google-shared-stuff", "favicon-kit",
    |                    "unavatar", "favicon-grabber"
    |
    */
    'default' => 'http',

    /*
    |--------------------------------------------------------------------------
    | Caching
    |--------------------------------------------------------------------------
    |
    | The package provides support for caching the fetched favicon's URLs.
    | Here, you can specify the different options for caching, such as
    | cache prefix that is prepended to all the cache keys.
    |
    */
    'cache' => [
        'prefix' => 'favicon-fetcher',
    ],

    /*
    |--------------------------------------------------------------------------
    | HTTP Timeouts
    |--------------------------------------------------------------------------
    |
    | Set the timeouts here in seconds for the HTTP requests that are made
    | to fetch the favicons. If the timeout is set to 0, then no timeout
    | will be applied. The connect timeout is the time taken to connect
    | to the server, while the timeout is the time taken to get a
    | response from the server after the connection is made.
    |
    */
    'timeout' => 0,

    'connect_timeout' => 0,

    /*
    |--------------------------------------------------------------------------
    | Verify TLS
    |--------------------------------------------------------------------------
    |
    | Sets the TLS verification option when making HTTP requests, which is
    | enabled by default.
    */
    'verify_tls' => env('FAVICON_FETCHER_VERIFY_TLS', true),

    /*
    |--------------------------------------------------------------------------
    | HTTP User Agent
    |--------------------------------------------------------------------------
    |
    | Set the user agent used by the HTTP client when fetching the favicons.
    |
    */
    'user_agent' => env('FAVICON_FETCHER_USER_AGENT'),
];


================================================
FILE: phpstan.neon
================================================
includes:
    - ./vendor/larastan/larastan/extension.neon

parameters:

    paths:
        - src

    level: 6

    ignoreErrors:
        - "#^Unsafe usage of new static#"
        - '#^Call to an undefined method AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection::fetch\(\)#'
        - '#^Call to an undefined method AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection::fetchAll\(\)#'



================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         backupGlobals="false"
         backupStaticAttributes="false"
         colors="true"
         verbose="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">src/</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
    </php>
</phpunit>


================================================
FILE: src/Collections/FaviconCollection.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Collections;

use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Favicon;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;

/**
 * @extends Collection<int, Favicon>
 */
class FaviconCollection extends Collection
{
    use HasDefaultFunctionality;

    /**
     * Whether the favicons in this collection were all retrieved from the cache.
     */
    protected bool $retrievedFromCache = false;

    /**
     * @param  array<array<int,string>>  $items
     * @return static
     */
    public static function makeFromCache(array $items = []): static
    {
        $collection = new static($items);

        $collection->retrievedFromCache = true;

        return $collection;
    }

    /**
     * Cache the collection of favicons. We only cache the collection if it contains
     * items and if it was not retrieved from the cache. If the collection was
     * retrieved from the cache, then the "force" flag has to be set to
     * true in order to cache it.
     */
    public function cache(CarbonInterface $ttl, bool $force = false): self
    {
        $shouldCache = $this->isNotEmpty()
            && ($force || ! $this->retrievedFromCache);

        if ($shouldCache) {
            $cacheKey = $this->buildCacheKeyForCollection($this->first()->getUrl());

            $cacheData = $this->map(fn (Favicon $favicon): array => $favicon->toCache())->all();

            Cache::put($cacheKey, $cacheData, $ttl);
        }

        return $this;
    }

    /**
     * Get the favicon with the largest icon size. Any icons with an unknown size (null)
     * will be treated as having a size of 0.
     */
    public function largest(): ?Favicon
    {
        return $this->sortByDesc(
            fn (Favicon $favicon): ?int => $favicon->getIconSize()
        )->first();
    }

    public function largestByFileSize(): ?Favicon
    {
        return $this->sortByDesc(
            fn (Favicon $favicon): int => strlen($favicon->content())
        )->first();
    }
}


================================================
FILE: src/Concerns/BuildsCacheKeys.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Concerns;

trait BuildsCacheKeys
{
    /**
     * Build the key used for caching the favicon.
     *
     * @param  string  $url
     * @return string
     */
    public function buildCacheKey(string $url): string
    {
        $url = str_replace(['http://', 'https://'], '', $url);

        return config('favicon-fetcher.cache.prefix').'.'.$url;
    }

    /**
     * Build the key used for caching the favicon collection.
     *
     * @param  string  $url
     * @return string
     */
    public function buildCacheKeyForCollection(string $url): string
    {
        return $this->buildCacheKey($url).'.collection';
    }
}


================================================
FILE: src/Concerns/HasDefaultFunctionality.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Concerns;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Facades\Favicon;
use AshAllenDesign\FaviconFetcher\Favicon as FetchedFavicon;
use Illuminate\Support\Facades\Cache;

trait HasDefaultFunctionality
{
    use BuildsCacheKeys;

    /**
     * An array of the drivers that should be as fallbacks if the current
     * driver fails to retrieve a favicon for the given URL.
     *
     * @var string[]
     */
    protected array $fallbacks = [];

    /**
     * Whether to throw an exception if the favicon cannot be found.
     *
     * @var bool
     */
    protected bool $throwOnNotFound = false;

    /**
     * Whether to attempt to retrieve the favicon URL from the cache.
     *
     * @var bool
     */
    protected bool $useCache = true;

    /**
     * Attempt to fetch the favicon for the given URL. If a favicon cannot
     * be found, return the default as a fallback.
     *
     * @param  string  $url
     * @param  mixed  $default
     * @return mixed
     *
     * @throws FaviconNotFoundException
     * @throws InvalidUrlException
     */
    public function fetchOr(string $url, mixed $default): mixed
    {
        if ($favicon = $this->fetch($url)) {
            return $favicon;
        }

        return $default instanceof \Closure ? $default($url) : $default;
    }

    /**
     * Attempt to fetch all the favicons for the given URL. If the favicons cannot
     * be found, return the default as a fallback.
     *
     * @param  string  $url
     * @param  mixed  $default
     * @return mixed
     *
     * @throws FaviconNotFoundException
     * @throws InvalidUrlException
     * @throws FeatureNotSupportedException
     */
    public function fetchAllOr(string $url, mixed $default): mixed
    {
        $favicons = $this->fetchAll($url);

        if ($favicons->isNotEmpty()) {
            return $favicons;
        }

        return $default instanceof \Closure ? $default($url) : $default;
    }

    /**
     * Specify whether to throw an exception if the favicon cannot be found.
     *
     * @param  bool  $throw
     * @return $this
     */
    public function throw(bool $throw = true): self
    {
        $this->throwOnNotFound = $throw;

        return $this;
    }

    /**
     * Specify which drivers should be used as fallbacks if the current
     * driver cannot find the favicon.
     *
     * @param  string  ...$fallbacks
     * @return $this
     */
    public function withFallback(string ...$fallbacks): self
    {
        $this->fallbacks = array_merge($this->fallbacks, $fallbacks);

        return $this;
    }

    /**
     * Specify whether to attempt to read the favicon from the cache.
     *
     * @param  bool  $useCache
     * @return $this
     */
    public function useCache(bool $useCache = true): self
    {
        $this->useCache = $useCache;

        return $this;
    }

    /**
     * Handle what happens if the favicon cannot be found using the current
     * driver. If any fallbacks are specified, attempt to find a favicon
     * using a different driver. If we have specified to throw an
     * exception, then do so. Otherwise, return null.
     *
     * @param  string  $url
     * @return FetchedFavicon|null
     *
     * @throws FaviconNotFoundException
     */
    protected function notFound(string $url)
    {
        if ($favicon = $this->attemptFallbacks($url)) {
            return $favicon;
        }

        if ($this->throwOnNotFound) {
            throw new FaviconNotFoundException('A favicon cannot be found for '.$url);
        }

        return null;
    }

    /**
     * Loop through each fallback driver and attempt to retrieve a favicon.
     *
     * @param  string  $url
     * @return FetchedFavicon|null
     */
    protected function attemptFallbacks(string $url): ?FetchedFavicon
    {
        foreach ($this->fallbacks as $driver) {
            if ($favicon = Favicon::driver($driver)->fetch($url)) {
                return $favicon;
            }
        }

        return null;
    }

    /**
     * Return the cached favicon, if one exists, or return null.
     *
     * @param  string  $url
     * @return FetchedFavicon|null
     *
     * @throws FaviconFetcherException
     */
    protected function attemptToFetchFromCache(string $url): ?FetchedFavicon
    {
        $cachedFaviconData = Cache::get($this->buildCacheKey($url));

        if (! $cachedFaviconData) {
            return null;
        }

        // If the cached data is still stored in the older format used in
        // v1 of the package, then we convert it to the new format. In
        // v3 of the package, we will remove this check and enforce
        // an array to be stored.
        if (is_string($cachedFaviconData)) {
            $cachedFaviconData = [
                'favicon_url' => $cachedFaviconData,
                'icon_type' => FetchedFavicon::TYPE_ICON_UNKNOWN,
                'icon_size' => null,
            ];
        }

        return (new FetchedFavicon(
            url: $url,
            faviconUrl: $cachedFaviconData['favicon_url'],
            retrievedFromCache: true,
        ))
            ->setIconType($cachedFaviconData['icon_type'])
            ->setIconSize($cachedFaviconData['icon_size']);
    }

    /**
     * Return a collection of cached favicons if they exist, or return null.
     *
     * @param  string  $url
     * @return FaviconCollection|null
     *
     * @throws FaviconFetcherException
     */
    protected function attemptToFetchCollectionFromCache(string $url): ?FaviconCollection
    {
        $cachedFaviconsData = Cache::get($this->buildCacheKeyForCollection($url));

        if (! $cachedFaviconsData) {
            return null;
        }

        $favicons = new FaviconCollection();

        foreach ($cachedFaviconsData as $cachedFaviconData) {
            $favicons->push((new FetchedFavicon(
                url: $url,
                faviconUrl: $cachedFaviconData['favicon_url'],
                retrievedFromCache: true,
            ))
                ->setIconType($cachedFaviconData['icon_type'])
                ->setIconSize($cachedFaviconData['icon_size']));
        }

        return $favicons;
    }
}


================================================
FILE: src/Concerns/MakesHttpRequests.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Concerns;

use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use Illuminate\Http\Client\ConnectionException as ClientConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

trait MakesHttpRequests
{
    protected function httpClient(): PendingRequest
    {
        $client = Http::timeout(config('favicon-fetcher.timeout'))
            ->connectTimeout(config('favicon-fetcher.connect_timeout'));

        if ($userAgent = config('favicon-fetcher.user_agent')) {
            $client->withUserAgent($userAgent);
        }

        if (! config('favicon-fetcher.verify_tls')) {
            $client->withoutVerifying();
        }

        return $client;
    }

    protected function withRequestExceptionHandling(\Closure $callback): mixed
    {
        try {
            return $callback();
        } catch (ClientConnectionException $exception) {
            throw new ConnectionException(
                $exception->getMessage(),
                $exception->getCode(),
                $exception
            );
        }
    }
}


================================================
FILE: src/Concerns/ValidatesUrls.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Concerns;

trait ValidatesUrls
{
    /**
     * Validate that the given parameter is a valid URL.
     *
     * @param  string  $url
     * @return bool
     */
    protected function urlIsValid(string $url): bool
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }
}


================================================
FILE: src/Contracts/Fetcher.php
================================================
<?php

namespace AshAllenDesign\FaviconFetcher\Contracts;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;

interface Fetcher
{
    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     */
    public function fetch(string $url): ?Favicon;

    /**
     * Attempt to fetch all favicons and icons for the given URL.
     *
     * @param  string  $url
     * @return FaviconCollection
     */
    public function fetchAll(string $url): FaviconCollection;

    /**
     * Attempt to fetch the favicon for the given URL. If a favicon cannot
     * be found, return the default as a fallback.
     *
     * @param  string  $url
     * @param  mixed  $default
     * @return mixed
     *
     * @throws FaviconNotFoundException
     * @throws InvalidUrlException
     */
    public function fetchOr(string $url, mixed $default): mixed;

    /**
     * Attempt to fetch all the favicons for the given URL. If the favicons cannot
     * be found, return the default as a fallback.
     *
     * @param  string  $url
     * @param  mixed  $default
     * @return mixed
     *
     * @throws FaviconNotFoundException
     * @throws InvalidUrlException
     * @throws FeatureNotSupportedException
     */
    public function fetchAllOr(string $url, mixed $default): mixed;
}


================================================
FILE: src/Drivers/DuckDuckGoDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Concerns\ValidatesUrls;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Illuminate\Http\Client\Response;

class DuckDuckGoDriver implements Fetcher
{
    use ValidatesUrls;
    use HasDefaultFunctionality;
    use MakesHttpRequests;

    private const BASE_URL = 'https://icons.duckduckgo.com/ip3/';

    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws FaviconNotFoundException
     * @throws InvalidUrlException
     * @throws ConnectionException
     * @throws FaviconFetcherException
     */
    public function fetch(string $url): ?Favicon
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
            return $favicon;
        }

        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);

        $faviconUrl = self::BASE_URL.$urlWithoutProtocol.'.ico';

        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($faviconUrl)
        );

        return $response->successful()
            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
            : $this->notFound($url);
    }

    public function fetchAll(string $url): FaviconCollection
    {
        throw new FeatureNotSupportedException('The DuckDuckGo driver does not support fetching all favicons.');
    }
}


================================================
FILE: src/Drivers/FaviconGrabberDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Concerns\ValidatesUrls;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Illuminate\Http\Client\Response;

class FaviconGrabberDriver implements Fetcher
{
    use ValidatesUrls;
    use HasDefaultFunctionality;
    use MakesHttpRequests;

    private const BASE_URL = 'https://favicongrabber.com/api/grab/';

    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws InvalidUrlException
     * @throws FaviconNotFoundException
     * @throws ConnectionException
     * @throws FaviconFetcherException
     */
    public function fetch(string $url): ?Favicon
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
            return $favicon;
        }

        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);

        $apiUrl = self::BASE_URL.$urlWithoutProtocol;

        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($apiUrl)
        );

        if (! $response->successful() || count($response->json('icons')) === 0) {
            return $this->notFound($url);
        }

        $faviconUrl = $response->json('icons')[0]['src'];

        return new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this);
    }

    public function fetchAll(string $url): FaviconCollection
    {
        throw new FeatureNotSupportedException('The FaviconGrabber driver does not support fetching all favicons.');
    }
}


================================================
FILE: src/Drivers/FaviconKitDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Concerns\ValidatesUrls;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Illuminate\Http\Client\Response;

class FaviconKitDriver implements Fetcher
{
    use ValidatesUrls;
    use HasDefaultFunctionality;
    use MakesHttpRequests;

    private const BASE_URL = 'https://api.faviconkit.com/';

    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws InvalidUrlException
     * @throws FaviconNotFoundException
     * @throws FaviconFetcherException
     */
    public function fetch(string $url): ?Favicon
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
            return $favicon;
        }

        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);

        $faviconUrl = self::BASE_URL.$urlWithoutProtocol;

        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($faviconUrl)
        );

        return $response->successful()
            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
            : $this->notFound($url);
    }

    public function fetchAll(string $url): FaviconCollection
    {
        throw new FeatureNotSupportedException('The FaviconKit API does not support fetching all favicons.');
    }
}


================================================
FILE: src/Drivers/GoogleSharedStuffDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Concerns\ValidatesUrls;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Illuminate\Http\Client\Response;

class GoogleSharedStuffDriver implements Fetcher
{
    use ValidatesUrls;
    use HasDefaultFunctionality;
    use MakesHttpRequests;

    private const BASE_URL = 'https://www.google.com/s2/favicons?domain=';

    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws FaviconNotFoundException
     * @throws InvalidUrlException
     * @throws ConnectionException
     * @throws FaviconFetcherException
     */
    public function fetch(string $url): ?Favicon
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
            return $favicon;
        }

        $faviconUrl = self::BASE_URL.$url;

        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($faviconUrl)
        );

        return $response->successful()
            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
            : $this->notFound($url);
    }

    public function fetchAll(string $url): FaviconCollection
    {
        throw new FeatureNotSupportedException('The Google Shared Stuff API does not support fetching all favicons.');
    }
}


================================================
FILE: src/Drivers/HttpDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Concerns\ValidatesUrls;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidIconSizeException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidIconTypeException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Illuminate\Http\Client\Response;
use Symfony\Component\DomCrawler\Crawler;

class HttpDriver implements Fetcher
{
    use ValidatesUrls;
    use HasDefaultFunctionality;
    use MakesHttpRequests;

    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws FaviconNotFoundException
     * @throws InvalidIconSizeException
     * @throws InvalidIconTypeException
     * @throws InvalidUrlException
     * @throws FaviconFetcherException
     */
    public function fetch(string $url): ?Favicon
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
            return $favicon;
        }

        $favicon = $this->attemptToResolveFromHeadTags($url)
            ?? new Favicon(url: $url, faviconUrl: $this->guessDefaultUrl($url), fromDriver: $this);

        $faviconCanBeReached = $this->faviconUrlCanBeReached($favicon->getFaviconUrl());

        return $faviconCanBeReached
            ? $favicon
            : $this->notFound($url);
    }

    public function fetchAll(string $url): FaviconCollection
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicons = $this->attemptToFetchCollectionFromCache($url)) {
            return $favicons;
        }

        $favicons = $this->attemptToResolveAllFromHeadTags($url);

        // If the URL couldn't be reached, throw and exception and return
        // an empty FaviconCollection.
        if ($favicons === null) {
            if ($this->throwOnNotFound) {
                throw new FaviconNotFoundException('A favicon cannot be found for '.$url);
            }

            $favicons = new FaviconCollection();
        }

        if ($favicons->isEmpty()) {
            $favicons->push(new Favicon(url: $url, faviconUrl: $this->guessDefaultUrl($url), fromDriver: $this));
        }

        // Return a FaviconCollection of favicons that can be reached.
        return $favicons->filter(
            fn (Favicon $favicon): bool => $this->faviconUrlCanBeReached($favicon->getFaviconUrl())
        );
    }

    /**
     * Attempt to resolve a favicon from the given URL. If the response
     * is successful, we can assume that a valid favicon was returned.
     * Otherwise, we can assume that a favicon wasn't found.
     *
     * @param  string  $faviconUrl
     * @return bool
     *
     * @throws ConnectionException
     */
    private function faviconUrlCanBeReached(string $faviconUrl): bool
    {
        return $this->withRequestExceptionHandling(
            fn (): bool => $this->httpClient()
                ->get($faviconUrl)
                ->successful()
        );
    }

    /**
     * Parse the HTML returned from the URL and attempt to find a favicon
     * specified using the "icon" or "shortcut icon" link tag. If one
     * is found, return the absolute URL of the link's "href".
     * Otherwise, return null.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws InvalidIconSizeException
     * @throws InvalidIconTypeException
     * @throws ConnectionException
     */
    private function attemptToResolveFromHeadTags(string $url): ?Favicon
    {
        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($url)
        );

        if (! $response->successful()) {
            return null;
        }

        $linkTag = (new Crawler($response->body()))
            ->filter('
                head link[rel="icon"][href],
                head link[rel="shortcut icon"][href]
            ')
            ->first();

        if (! $linkTag->count()) {
            return null;
        }

        $favicon = new Favicon(
            url: $url,
            faviconUrl: $this->convertToAbsoluteUrl($url, $linkTag->attr('href')),
            fromDriver: $this,
        );

        if ($iconSize = $linkTag->attr('sizes')) {
            $favicon->setIconSize((int) $iconSize);
        }

        if ($iconType = $this->guessTypeFromElement($linkTag)) {
            $favicon->setIconType($iconType);
        }

        return $favicon;
    }

    /**
     * @throws ConnectionException
     */
    private function attemptToResolveAllFromHeadTags(string $url): ?FaviconCollection
    {
        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($url)
        );

        if (! $response->successful()) {
            return null;
        }

        $linkTags = (new Crawler($response->body()))
            ->filter('
                head link[rel="icon"][href],
                head link[rel="shortcut icon"][href],
                head link[rel="apple-touch-icon"][href]
            ');

        if (! $linkTags->count()) {
            return null;
        }

        $favicons = $linkTags->each(function (Crawler $linkTag) use ($url): Favicon {
            $favicon = new Favicon(
                $url,
                $this->convertToAbsoluteUrl($url, $linkTag->attr('href')),
                $this,
            );

            if ($iconSize = $linkTag->attr('sizes')) {
                $favicon->setIconSize((int) $iconSize);
            }

            if ($iconType = $this->guessTypeFromElement($linkTag)) {
                $favicon->setIconType($iconType);
            }

            return $favicon;
        });

        return new FaviconCollection($favicons);
    }

    private function guessTypeFromElement(Crawler $linkElement): string
    {
        return match ($linkElement->attr('rel')) {
            'icon' => Favicon::TYPE_ICON,
            'shortcut icon' => Favicon::TYPE_SHORTCUT_ICON,
            'apple-touch-icon' => Favicon::TYPE_APPLE_TOUCH_ICON,
            default => Favicon::TYPE_ICON_UNKNOWN,
        };
    }

    /**
     * Convert the favicon URL to be absolute rather than relative.
     *
     * @param  string  $baseUrl
     * @param  string  $faviconUrl
     * @return string
     */
    private function convertToAbsoluteUrl(string $baseUrl, string $faviconUrl): string
    {
        // If the favicon URL is relative, we need to convert it to be absolute.
        // We also strip the path (if there is one) from the base URL.
        if (! filter_var($faviconUrl, FILTER_VALIDATE_URL)) {
            $faviconUrl = $this->stripPathFromUrl($baseUrl).'/'.ltrim($faviconUrl, '/');
        }

        return $faviconUrl;
    }

    /**
     * Build and return the default path where we can guess the favicon
     * file might be stored.
     *
     * @param  string  $url
     * @return string
     */
    private function guessDefaultUrl(string $url): string
    {
        return rtrim($this->stripPathFromUrl($url)).'/favicon.ico';
    }

    /**
     * Strip the path and any query parameters from the given URL so that
     * we only return the scheme, host and port (if there is one).
     *
     * @param  string  $url
     * @return string
     */
    private function stripPathFromUrl(string $url): string
    {
        $parsedUrl = parse_url($url);

        $url = $parsedUrl['scheme'].'://'.$parsedUrl['host'];

        if (array_key_exists('port', $parsedUrl)) {
            $url .= ':'.$parsedUrl['port'];
        }

        return $url;
    }
}


================================================
FILE: src/Drivers/UnavatarDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Concerns\HasDefaultFunctionality;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Concerns\ValidatesUrls;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\FeatureNotSupportedException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Illuminate\Http\Client\Response;

class UnavatarDriver implements Fetcher
{
    use ValidatesUrls;
    use HasDefaultFunctionality;
    use MakesHttpRequests;

    private const BASE_URL = 'https://unavatar.io/';

    /**
     * Attempt to fetch the favicon for the given URL.
     *
     * @param  string  $url
     * @return Favicon|null
     *
     * @throws InvalidUrlException
     * @throws FaviconNotFoundException
     * @throws FaviconFetcherException
     */
    public function fetch(string $url): ?Favicon
    {
        if (! $this->urlIsValid($url)) {
            throw new InvalidUrlException($url.' is not a valid URL');
        }

        if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
            return $favicon;
        }

        $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);

        $faviconUrl = self::BASE_URL.$urlWithoutProtocol.'?fallback=false';

        $response = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($faviconUrl)
        );

        return $response->successful()
            ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
            : $this->notFound($url);
    }

    public function fetchAll(string $url): FaviconCollection
    {
        throw new FeatureNotSupportedException('The Unavatar API does not support fetching all favicons.');
    }
}


================================================
FILE: src/Exceptions/ConnectionException.php
================================================
<?php

namespace AshAllenDesign\FaviconFetcher\Exceptions;

class ConnectionException extends FaviconFetcherException
{
    //
}


================================================
FILE: src/Exceptions/FaviconFetcherException.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Exceptions;

use Exception;

class FaviconFetcherException extends Exception
{
    //
}


================================================
FILE: src/Exceptions/FaviconNotFoundException.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Exceptions;

class FaviconNotFoundException extends FaviconFetcherException
{
    //
}


================================================
FILE: src/Exceptions/FeatureNotSupportedException.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Exceptions;

class FeatureNotSupportedException extends FaviconFetcherException
{
    //
}


================================================
FILE: src/Exceptions/InvalidIconSizeException.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Exceptions;

class InvalidIconSizeException extends FaviconFetcherException
{
    //
}


================================================
FILE: src/Exceptions/InvalidIconTypeException.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Exceptions;

class InvalidIconTypeException extends FaviconFetcherException
{
    //
}


================================================
FILE: src/Exceptions/InvalidUrlException.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Exceptions;

class InvalidUrlException extends FaviconFetcherException
{
    //
}


================================================
FILE: src/Facades/Favicon.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Facades;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Favicon as FetchedFavicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use Illuminate\Support\Facades\Facade;

/**
 * @method static Fetcher driver(string $driver = null)
 * @method static void extend(string $name, Fetcher $fetcher)
 * @method static Fetcher throw(bool $throw = true)
 * @method static Fetcher withFallback(string ...$fallbacks)
 * @method static Fetcher useCache(bool $useCache = true)
 * @method static FetchedFavicon|null fetch(string $url)
 * @method static mixed fetchOr(string $url, mixed $default)
 * @method static FaviconCollection fetchAll(string $url)
 * @method static mixed fetchAllOr(string $url, mixed $default)
 *
 * @see FetcherManager
 */
class Favicon extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor(): string
    {
        return 'favicon-fetcher';
    }
}


================================================
FILE: src/Favicon.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher;

use AshAllenDesign\FaviconFetcher\Concerns\BuildsCacheKeys;
use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidIconSizeException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidIconTypeException;
use Carbon\CarbonInterface;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class Favicon
{
    use BuildsCacheKeys;
    use MakesHttpRequests;

    public const TYPE_ICON = 'icon';

    public const TYPE_SHORTCUT_ICON = 'shortcut_icon';

    public const TYPE_APPLE_TOUCH_ICON = 'apple_touch_icon';

    public const TYPE_ICON_UNKNOWN = 'unknown';

    /**
     * The URL of the website that the favicon belongs to.
     *
     * @var string
     */
    protected string $url;

    /**
     * The URL of the favicon.
     *
     * @var string
     */
    protected string $faviconUrl;

    /**
     * The driver that was used to fetch the favicon. If the favicon was
     * retrieved from the cache, this will be null.
     *
     * @var Fetcher|null
     */
    protected ?Fetcher $driver = null;

    /**
     * Whether the favicon's URL was retrieved from the cache.
     *
     * @var bool
     */
    protected bool $retrievedFromCache = false;

    protected string $iconType = self::TYPE_ICON_UNKNOWN;

    protected ?int $size = null;

    public function __construct(
        string $url,
        string $faviconUrl,
        ?Fetcher $fromDriver = null,
        bool $retrievedFromCache = false
    ) {
        $this->url = $url;
        $this->faviconUrl = $faviconUrl;
        $this->driver = $fromDriver;
        $this->retrievedFromCache = $retrievedFromCache;
    }

    public function setIconSize(?int $size): static
    {
        if ($size !== null && $size < 0) {
            throw new InvalidIconSizeException('The size ['.$size.'] is not a valid favicon size.');
        }

        $this->size = $size;

        return $this;
    }

    public function setIconType(string $type): static
    {
        if (! $this->acceptableIconType($type)) {
            throw new InvalidIconTypeException('The type ['.$type.'] is not a valid favicon type.');
        }

        $this->iconType = $type;

        return $this;
    }

    public function getUrl(): string
    {
        return $this->url;
    }

    public function getFaviconUrl(): string
    {
        return $this->faviconUrl;
    }

    public function retrievedFromCache(): bool
    {
        return $this->retrievedFromCache;
    }

    /**
     * Get the contents of the favicon file.
     *
     * @return string
     *
     * @throws ConnectionException
     */
    public function content(): string
    {
        return $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($this->faviconUrl)
        )->body();
    }

    /**
     * Cache the favicon URL. If the favicon is already cached, "force"
     * must be passed as "true" to re-cache the URL.
     *
     * @param  CarbonInterface  $ttl
     * @param  bool  $force
     * @return $this
     */
    public function cache(CarbonInterface $ttl, bool $force = false): self
    {
        if ($force || ! $this->retrievedFromCache) {
            Cache::put(
                $this->buildCacheKey($this->url),
                $this->toCache(),
                $ttl
            );
        }

        return $this;
    }

    /**
     * Store the favicon in storage using an automatically generate filename.
     *
     * @param  string  $directory
     * @param  string|null  $disk
     * @return string
     */
    public function store(string $directory, ?string $disk = null): string
    {
        return $this->storeAs($directory, Str::uuid()->toString(), $disk);
    }

    /**
     * Store the favicon in storage.
     *
     * @param  string  $directory
     * @param  string  $filename
     * @param  string|null  $disk
     * @return string
     */
    public function storeAs(string $directory, string $filename, ?string $disk = null): string
    {
        $path = $this->buildStoragePath($directory, $filename);

        Storage::disk($disk)->put($path, $this->content());

        return $path;
    }

    public function getIconType(): string
    {
        return $this->iconType;
    }

    public function getIconSize(): ?int
    {
        return $this->size;
    }

    protected function buildStoragePath(string $directory, string $filename): string
    {
        return Str::of($directory)
            ->append('/')
            ->append($filename)
            ->append('.')
            ->append($this->guessFileExtension())
            ->toString();
    }

    protected function guessFileExtension(): string
    {
        $default = File::extension($this->faviconUrl);

        if (Str::of($this->faviconUrl)->endsWith(['png', 'ico', 'svg'])) {
            return $default;
        }

        return $this->guessFileExtensionFromMimeType() ?? $default;
    }

    /**
     * @throws ConnectionException
     */
    protected function guessFileExtensionFromMimeType(): ?string
    {
        $faviconMimetype = $this->withRequestExceptionHandling(
            fn (): Response => $this->httpClient()->get($this->faviconUrl)
        )->header('content-type');

        $mimeToExtensionMap = [
            'image/x-icon' => 'ico',
            'image/x-ico' => 'ico',
            'image/vnd.microsoft.icon' => 'ico',
            'image/jpeg' => 'jpeg',
            'image/pjpeg' => 'jpeg',
            'image/png' => 'png',
            'image/x-png' => 'png',
            'image/svg+xml' => 'svg',
        ];

        return $mimeToExtensionMap[$faviconMimetype] ?? null;
    }

    private function acceptableIconType(string $type): bool
    {
        return in_array(
            needle: $type,
            haystack: [
                self::TYPE_ICON,
                self::TYPE_SHORTCUT_ICON,
                self::TYPE_APPLE_TOUCH_ICON,
                self::TYPE_ICON_UNKNOWN,
            ],
            strict: true);
    }

    /**
     * Transform the favicon object into an array that can be cached.
     *
     * @return array<string,string|int|null>
     */
    public function toCache(): array
    {
        return [
            'favicon_url' => $this->getFaviconUrl(),
            'icon_size' => $this->getIconSize(),
            'icon_type' => $this->getIconType(),
        ];
    }
}


================================================
FILE: src/FaviconFetcherProvider.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher;

use Illuminate\Support\ServiceProvider;

class FaviconFetcherProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/favicon-fetcher.php', 'favicon-fetcher');

        $this->app->bind('favicon-fetcher', fn () => new FetcherManager());
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../config/favicon-fetcher.php' => config_path('favicon-fetcher.php'),
        ], 'favicon-fetcher-config');
    }
}


================================================
FILE: src/FetcherManager.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher;

use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Drivers\DuckDuckGoDriver;
use AshAllenDesign\FaviconFetcher\Drivers\FaviconGrabberDriver;
use AshAllenDesign\FaviconFetcher\Drivers\FaviconKitDriver;
use AshAllenDesign\FaviconFetcher\Drivers\GoogleSharedStuffDriver;
use AshAllenDesign\FaviconFetcher\Drivers\HttpDriver;
use AshAllenDesign\FaviconFetcher\Drivers\UnavatarDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;

class FetcherManager
{
    /**
     * @var Fetcher[]
     */
    protected static array $customDrivers = [];

    public static function driver(?string $driver = null): Fetcher
    {
        $driver ??= config('favicon-fetcher.default');

        return match ($driver) {
            'http' => new HttpDriver(),
            'google-shared-stuff' => new GoogleSharedStuffDriver(),
            'favicon-kit' => new FaviconKitDriver(),
            'unavatar' => new UnavatarDriver(),
            'favicon-grabber' => new FaviconGrabberDriver(),
            'duck-duck-go' => new DuckDuckGoDriver(),
            default => static::attemptToCreateCustomDriver($driver),
        };
    }

    public static function extend(string $name, Fetcher $fetcher): void
    {
        self::$customDrivers[$name] = $fetcher;
    }

    protected static function attemptToCreateCustomDriver(string $driver): Fetcher
    {
        return static::$customDrivers[$driver]
            ?? throw new FaviconFetcherException($driver.' is not a valid driver.');
    }

    public function __call(string $method, mixed $parameters): mixed
    {
        return static::driver()->$method(...$parameters);
    }
}


================================================
FILE: tests/Feature/Collections/FaviconCollectionTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Collections;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;

final class FaviconCollectionTest extends TestCase
{
    use LazilyRefreshDatabase;

    /** @test */
    public function favicon_collection_can_be_cached_if_the_collection_was_not_retrieved_from_the_cache(): void
    {
        $collection = FaviconCollection::make([
            (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
        ]);

        $collection->cache(now()->addDay());

        $cachedItems = Cache::get('favicon-fetcher.example.com.collection');

        self::assertSame(
            expected: [
                [
                    'favicon_url' => 'https://example.com/images/apple-icon-180x180.png',
                    'icon_size' => 180,
                    'icon_type' => 'apple_touch_icon',
                ],
                [
                    'favicon_url' => 'https://example.com/images/favicon.ico',
                    'icon_size' => null,
                    'icon_type' => 'shortcut_icon',
                ],
            ],
            actual: $cachedItems
        );
    }

    /** @test */
    public function favicon_collection_can_be_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_true(): void
    {
        Cache::put(
            key: 'favicon-fetcher.example.com.collection',
            value: 'Dummy value here that should be overridden',
            ttl: now()->addDay(),
        );

        FaviconCollection::makeFromCache([
            (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
        ])->cache(now()->addDay(), true);

        // Assert that the items in the database were overridden.
        self::assertSame(
            expected: [
                [
                    'favicon_url' => 'https://example.com/images/apple-icon-180x180.png',
                    'icon_size' => 180,
                    'icon_type' => 'apple_touch_icon',
                ],
                [
                    'favicon_url' => 'https://example.com/images/favicon.ico',
                    'icon_size' => null,
                    'icon_type' => 'shortcut_icon',
                ],
            ],
            actual: Cache::get('favicon-fetcher.example.com.collection')
        );
    }

    /** @test */
    public function favicon_collection_is_not_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_false(): void
    {
        Cache::put(
            key: 'favicon-fetcher.example.com.collection',
            value: 'Dummy value here that should not be overridden',
            ttl: now()->addDay(),
        );

        FaviconCollection::makeFromCache([
            (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
        ])->cache(now()->addDay());

        // Assert that the items in the database were not overridden.
        self::assertSame(
            expected: 'Dummy value here that should not be overridden',
            actual: Cache::get('favicon-fetcher.example.com.collection')
        );
    }

    /** @test */
    public function favicon_collection_is_not_cached_if_the_collection_is_empty(): void
    {
        Cache::shouldReceive('put')->never();

        $collection = new FaviconCollection();

        $collection->cache(now()->addDay());
    }

    /** @test */
    public function largest_favicon_can_be_retrieved(): void
    {
        $largest = FaviconCollection::make([
            (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-57x57.png'))->setIconSize(57)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-60x60.png'))->setIconSize(60)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(72)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(76)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-76x76.png'))->setIconSize(114)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-120x120.png'))->setIconSize(120)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-144x144.png'))->setIconSize(144)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-152x152.png'))->setIconSize(152)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/android-icon-192x192.png'))->setIconSize(192)->setIconType(Favicon::TYPE_ICON),
        ])->largest();

        self::assertSame('https://example.com/favicon/android-icon-192x192.png', $largest->getFaviconUrl());
    }

    /** @test */
    public function largest_favicon_can_be_retrieved_if_there_are_only_null_sizes(): void
    {
        $largest = FaviconCollection::make([
            (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
            (new Favicon('https://example.com', 'https://example.com/favicon/favicon-64x64.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
        ])->largest();

        self::assertSame('https://example.com/favicon/favicon-32x32.png', $largest->getFaviconUrl());
    }

    /** @test */
    public function largest_favicon_can_be_retrieved_based_on_file_size()
    {
        // mock the favicons to specify file content lengths
        $favicon1 = $this->createMock(Favicon::class);
        $favicon1->method('getFaviconUrl')->willReturn('https://example.com/favicon/favicon-32x32.png');
        $favicon1->method('content')->willReturn('some-short-string');

        $favicon2 = $this->createMock(Favicon::class);
        $favicon2->method('getFaviconUrl')->willReturn('https://example.com/favicon/favicon-64x64.png');
        $favicon2->method('content')->willReturn('some-much-longer-string');

        $largest = FaviconCollection::make([
            $favicon1,
            $favicon2,
        ])->largestByFileSize();

        self::assertSame('https://example.com/favicon/favicon-64x64.png', $largest->getFaviconUrl());
    }
}


================================================
FILE: tests/Feature/Concerns/MakesHttpRequests/HttpClientTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Concerns\MakesHttpRequests;

use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;

final class HttpClientTest extends TestCase
{
    use MakesHttpRequests;

    /** @test */
    public function http_client_is_returned_with_correct_options(): void
    {
        config([
            'favicon-fetcher.timeout' => 10,
            'favicon-fetcher.connect_timeout' => 5,
            'favicon-fetcher.verify_tls' => false,
        ]);

        $client = $this->httpClient();

        self::assertEquals(10, $client->getOptions()['timeout']);
        self::assertEquals(5, $client->getOptions()['connect_timeout']);
        self::assertFalse($client->getOptions()['verify']);
    }

    /** @test */
    public function http_client_is_returned_with_correct_verify_tls_option(): void
    {
        config([
            'favicon-fetcher.verify_tls' => true,
        ]);

        $client = $this->httpClient();

        // The "verify" option shouldn't be present because we've not set it,
        // so if it's not in the array, we can make the assumption that it's
        // set to true under the hood by Laravel.
        self::assertArrayNotHasKey('verify', $client->getOptions());
    }
}


================================================
FILE: tests/Feature/Concerns/MakesHttpRequests/WithRequestExceptionHandlingTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Concerns\MakesHttpRequests;

use AshAllenDesign\FaviconFetcher\Concerns\MakesHttpRequests;
use AshAllenDesign\FaviconFetcher\Exceptions\ConnectionException;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Http\Client\ConnectionException as ClientConnectionException;

final class WithRequestExceptionHandlingTest extends TestCase
{
    use MakesHttpRequests;

    /** @test */
    public function exception_is_handled_and_rethrown(): void
    {
        $this->expectException(ConnectionException::class);

        $this->withRequestExceptionHandling(function () {
            throw new ClientConnectionException('Test exception');
        });
    }
}


================================================
FILE: tests/Feature/Drivers/DuckDuckGoDriverTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Drivers;

use AshAllenDesign\FaviconFetcher\Drivers\DuckDuckGoDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\NullDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class DuckDuckGoDriverTest extends TestCase
{
    use LazilyRefreshDatabase;

    /**
     * @test
     *
     * @testWith ["https"]
     *           ["http"]
     */
    public function favicon_can_be_fetched_from_driver(string $protocol): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new DuckDuckGoDriver())->fetch($protocol.'://example.com');

        self::assertSame('https://icons.duckduckgo.com/ip3/example.com.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.example.com',
            [
                'favicon_url' => 'url-goes-here',
                'icon_size' => null,
                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
            ],
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new DuckDuckGoDriver())->fetch('https://example.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.https://example.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new DuckDuckGoDriver())->useCache(false)->fetch('https://example.com');

        self::assertSame('https://icons.duckduckgo.com/ip3/example.com.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new DuckDuckGoDriver())->useCache(true)->fetch('https://example.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new CustomDriver());

        $favicon = (new DuckDuckGoDriver())
            ->withFallback('custom-driver')
            ->useCache(true)
            ->fetch('https://example.com');

        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new DuckDuckGoDriver())
                ->throw()
                ->useCache(true)
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method(): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new DuckDuckGoDriver())
            ->useCache(true)
            ->fetchOr('https://example.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new DuckDuckGoDriver())
            ->fetchOr('https://example.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function exception_can_be_thrown_after_attempting_a_fallback(): void
    {
        Http::fake([
            'https://icons.duckduckgo.com/ip3/example.com.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new NullDriver());

        $exception = null;

        try {
            (new DuckDuckGoDriver())
                ->throw()
                ->withFallback('custom-driver')
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());

        self::assertTrue(NullDriver::$flag);
    }

    /** @test */
    public function exception_is_thrown_if_the_url_is_invalid(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new DuckDuckGoDriver())->fetch('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }
}


================================================
FILE: tests/Feature/Drivers/FaviconGrabberDriverTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Drivers;

use AshAllenDesign\FaviconFetcher\Drivers\FaviconGrabberDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\NullDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class FaviconGrabberDriverTest extends TestCase
{
    use LazilyRefreshDatabase;

    /**
     * @test
     *
     * @testWith ["https"]
     *           ["http"]
     */
    public function favicon_can_be_fetched_from_driver(string $protocol): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/aws.amazon.com' => Http::response($this->successfulResponseBody()),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())->fetch($protocol.'://aws.amazon.com');

        self::assertSame('https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.aws.amazon.com',
            [
                'favicon_url' => 'url-goes-here',
                'icon_size' => null,
                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
            ],
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())->fetch('https://aws.amazon.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.https://aws.amazon.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            'https://favicongrabber.com/api/grab/aws.amazon.com' => Http::response($this->successfulResponseBody()),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())->useCache(false)->fetch('https://aws.amazon.com');

        self::assertSame('https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/empty.com' => Http::response($this->successfulEmptyResponseBody()),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())->useCache(true)->fetch('https://empty.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function null_is_returned_if_the_domain_is_invalid(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())->useCache(true)->fetch('https://invalid.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new CustomDriver());

        $favicon = (new FaviconGrabberDriver())
            ->withFallback('custom-driver')
            ->useCache(true)
            ->fetch('https://invalid.com');

        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new FaviconGrabberDriver())
                ->throw()
                ->useCache(true)
                ->fetch('https://invalid.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://invalid.com', $exception->getMessage());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())
            ->useCache(true)
            ->fetchOr('https://invalid.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconGrabberDriver())
            ->fetchOr('https://invalid.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function exception_can_be_thrown_after_attempting_a_fallback(): void
    {
        Http::fake([
            'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new NullDriver());

        $exception = null;

        try {
            (new FaviconGrabberDriver())
                ->throw()
                ->withFallback('custom-driver')
                ->fetch('https://invalid.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://invalid.com', $exception->getMessage());

        self::assertTrue(NullDriver::$flag);
    }

    /** @test */
    public function exception_is_thrown_if_the_url_is_invalid(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new FaviconGrabberDriver())->fetch('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }

    private function successfulResponseBody(): array
    {
        return [
            'domain' => 'aws.amazon.com',
            'icons' => [
                [
                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico',
                    'type' => 'image/ico',
                ],
                [
                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico',
                    'type' => 'image/ico',
                ],
                [
                    'sizes' => '57x57',
                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-iphone-114-smile.png',
                ],
                [
                    'sizes' => '72x72',
                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-ipad-144-smile.png',
                ],
                [
                    'sizes' => '114x114',
                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-iphone-114-smile.png',
                ],
                [
                    'sizes' => '144x144',
                    'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-ipad-144-smile.png',
                ],
                [
                    'src' => 'https://aws.amazon.com/favicon.ico',
                    'type' => 'image/x-icon',
                ],
            ],
        ];
    }

    private function successfulEmptyResponseBody(): array
    {
        return [
            'domain' => 'empty.com',
            'icons' => [],
        ];
    }

    private function domainNotFoundResponseBody(): array
    {
        return [
            'error' => 'Unresolved domain name.',
        ];
    }
}


================================================
FILE: tests/Feature/Drivers/FaviconKitDriverTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Drivers;

use AshAllenDesign\FaviconFetcher\Drivers\FaviconKitDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\NullDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class FaviconKitDriverTest extends TestCase
{
    use LazilyRefreshDatabase;

    /**
     * @test
     *
     * @testWith ["https"]
     *           ["http"]
     */
    public function favicon_can_be_fetched_from_driver(string $protocol): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconKitDriver())->fetch($protocol.'://example.com');

        self::assertSame('https://api.faviconkit.com/example.com', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.example.com',
            [
                'favicon_url' => 'url-goes-here',
                'icon_size' => null,
                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
            ],
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconKitDriver())->fetch('https://example.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.https://example.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconKitDriver())->useCache(false)->fetch('https://example.com');

        self::assertSame('https://api.faviconkit.com/example.com', $favicon->getFaviconUrl());
    }

    /** @test */
    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconKitDriver())->useCache(true)->fetch('https://example.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new CustomDriver());

        $favicon = (new FaviconKitDriver())
            ->withFallback('custom-driver')
            ->useCache(true)
            ->fetch('https://example.com');

        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new FaviconKitDriver())
                ->throw()
                ->useCache(true)
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method(): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconKitDriver())
            ->useCache(true)
            ->fetchOr('https://example.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new FaviconKitDriver())
            ->fetchOr('https://example.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function exception_can_be_thrown_after_attempting_a_fallback(): void
    {
        Http::fake([
            'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new NullDriver());

        $exception = null;

        try {
            (new FaviconKitDriver())
                ->throw()
                ->withFallback('custom-driver')
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());

        self::assertTrue(NullDriver::$flag);
    }

    /** @test */
    public function exception_is_thrown_if_the_url_is_invalid(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new FaviconKitDriver())->fetch('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }
}


================================================
FILE: tests/Feature/Drivers/GoogleSharedStuffDriverTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Drivers;

use AshAllenDesign\FaviconFetcher\Drivers\GoogleSharedStuffDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\NullDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class GoogleSharedStuffDriverTest extends TestCase
{
    use LazilyRefreshDatabase;

    /**
     * @test
     *
     * @testWith ["https"]
     *           ["http"]
     */
    public function favicon_can_be_fetched_from_driver(string $protocol): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain='.$protocol.'://example.com' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new GoogleSharedStuffDriver())->fetch($protocol.'://example.com');

        self::assertSame('https://www.google.com/s2/favicons?domain='.$protocol.'://example.com', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.example.com',
            [
                'favicon_url' => 'url-goes-here',
                'icon_size' => null,
                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
            ],
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new GoogleSharedStuffDriver())->fetch('https://example.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.https://example.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new GoogleSharedStuffDriver())->useCache(false)->fetch('https://example.com');

        self::assertSame('https://www.google.com/s2/favicons?domain=https://example.com', $favicon->getFaviconUrl());
    }

    /** @test */
    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new GoogleSharedStuffDriver())->useCache(true)->fetch('https://example.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new CustomDriver());

        $favicon = (new GoogleSharedStuffDriver())
            ->withFallback('custom-driver')
            ->useCache(true)
            ->fetch('https://example.com');

        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new GoogleSharedStuffDriver())
                ->throw()
                ->useCache(true)
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method(): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new GoogleSharedStuffDriver())
            ->useCache(true)
            ->fetchOr('https://example.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new GoogleSharedStuffDriver())
            ->fetchOr('https://example.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function exception_can_be_thrown_after_attempting_a_fallback(): void
    {
        Http::fake([
            'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new NullDriver());

        $exception = null;

        try {
            (new GoogleSharedStuffDriver())
                ->throw()
                ->withFallback('custom-driver')
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());

        self::assertTrue(NullDriver::$flag);
    }

    /** @test */
    public function exception_is_thrown_if_the_url_is_invalid(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new GoogleSharedStuffDriver())->fetch('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }
}


================================================
FILE: tests/Feature/Drivers/HttpDriverTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Drivers;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Drivers\HttpDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\NullDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class HttpDriverTest extends TestCase
{
    use LazilyRefreshDatabase;

    /**
     * @test
     *
     * @dataProvider faviconLinksInHtmlProvider
     */
    public function favicon_can_be_fetched_using_link_element_in_html(
        string $html,
        string $expectedFaviconUrl,
        ?int $expectedSize,
        string $expectedType,
    ): void {
        Http::preventStrayRequests();

        Http::fake([
            'https://example.com' => Http::response($html),
            $expectedFaviconUrl => Http::response('favicon contents here'),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com');

        self::assertSame($expectedFaviconUrl, $favicon->getFaviconUrl());
        self::assertSame($expectedSize, $favicon->getIconSize());
        self::assertSame($expectedType, $favicon->getIconType());
    }

    /** @test */
    public function favicon_can_be_fetched_if_the_url_has_a_path_and_thelink_element_contains_a_relative_url(): void
    {
        Http::fake([
            'https://example.com/blog' => Http::response(self::htmlOptionOne()),
            'https://example.com/icon/is/here.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com/blog');

        self::assertSame('https://example.com/icon/is/here.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html(): void
    {
        $responseHtml = <<<'HTML'
            <html lang="en">
                <link rel="localization" href="branding/brand.ftl" />
            </html>
        HTML;

        Http::fake([
            'https://example.com' => Http::response($responseHtml),
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com');

        self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_and_a_relative_url_is_passed(): void
    {
        $responseHtml = <<<'HTML'
            <html lang="en">
                <link rel="localization" href="branding/brand.ftl" />
            </html>
        HTML;

        Http::fake([
            'https://example.com/blog' => Http::response($responseHtml),
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com/blog');

        self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());
    }

    /**
     * @test
     *
     * @testWith ["https"]
     *           ["http"]
     */
    public function favicon_can_be_fetched_from_driver(string $protocol): void
    {
        Http::fake([
            'https://example.com' => Http::response('<link href="/icon/favicon.ico" rel="icon">'),
            'http://example.com' => Http::response('<link href="/icon/favicon.ico" rel="icon">'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch($protocol.'://example.com');

        self::assertSame($protocol.'://example.com/icon/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_url_with_port(): void
    {
        Http::fake([
            'http://example.com:8080' => Http::response('<link href="/icon/favicon.ico" rel="icon">'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('http://example.com:8080');

        self::assertSame('http://example.com:8080/icon/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_url_with_query_parameters(): void
    {
        Http::fake([
            'http://example.com?query=parameter' => Http::response('<link href="/icon/favicon.ico" rel="icon">'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('http://example.com?query=parameter');

        self::assertSame('http://example.com/icon/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.example.com',
            [
                'favicon_url' => 'url-goes-here',
                'icon_size' => null,
                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
            ],
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
        self::assertNull($favicon->getIconSize());
        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicon->getIconType());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists_in_the_old_string_format(): void
    {
        Cache::put(
            'favicon-fetcher.example.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
        self::assertNull($favicon->getIconSize());
        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicon->getIconType());
    }

    /** @test */
    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.https://example.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            'https://example.com' => Http::response('<link href="/icon/favicon.ico" rel="icon">'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->useCache(false)->fetch('https://example.com');

        self::assertSame('https://example.com/icon/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())->useCache(true)->fetch('https://example.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new CustomDriver());

        $favicon = (new HttpDriver())
            ->withFallback('custom-driver')
            ->useCache(true)
            ->fetch('https://example.com');

        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new HttpDriver())
                ->throw()
                ->useCache(true)
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())
            ->useCache(true)
            ->fetchOr('https://example.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())
            ->fetchOr('https://example.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function exception_can_be_thrown_after_attempting_a_fallback(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new NullDriver());

        $exception = null;

        try {
            (new HttpDriver())
                ->throw()
                ->withFallback('custom-driver')
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());

        self::assertTrue(NullDriver::$flag);
    }

    /** @test */
    public function exception_is_thrown_if_the_url_is_invalid(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new HttpDriver())->fetch('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }

    /**
     * @test
     *
     * @dataProvider allFaviconLinksInHtmlProvider
     */
    public function all_icons_for_a_url_can_be_fetched(string $html, $expectedFaviconCollection): void
    {
        Http::fake([
            'https://example.com' => Http::response($html),
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->fetchAll('https://example.com');

        self::assertCount($expectedFaviconCollection->count(), $favicons);

        foreach ($favicons as $index => $favicon) {
            self::assertSame($expectedFaviconCollection[$index]->getFaviconUrl(), $favicon->getFaviconUrl());
            self::assertSame($expectedFaviconCollection[$index]->getIconType(), $favicon->getIconType());
            self::assertSame($expectedFaviconCollection[$index]->getIconSize(), $favicon->getIconSize());
        }
    }

    /** @test */
    public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_when_trying_to_get_all_icons(): void
    {
        $responseHtml = <<<'HTML'
            <html lang="en">
                <link rel="localization" href="branding/brand.ftl" />
            </html>
        HTML;

        Http::fake([
            'https://example.com' => Http::response($responseHtml),
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->fetchAll('https://example.com');

        self::assertCount(1, $favicons);
        self::assertSame($favicons->first()->getFaviconUrl(), 'https://example.com/favicon.ico');
    }

    /** @test */
    public function empty_favicon_collection_is_returned_if_the_url_cannot_be_reached(): void
    {
        Http::fake([
            'https://example.com' => Http::response('not found', 404),
            'https://example.com/favicon.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->fetchAll('https://example.com');

        self::assertCount(0, $favicons);
    }

    /** @test */
    public function empty_favicon_collection_is_returned_if_no_icons_can_be_found_for_a_url(): void
    {
        $responseHtml = <<<'HTML'
            <html lang="en">
                <link rel="localization" href="branding/brand.ftl" />
            </html>
        HTML;

        Http::fake([
            'https://example.com' => Http::response($responseHtml),
            'https://example.com/favicon.ico' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->fetchAll('https://example.com');

        self::assertCount(0, $favicons);
    }

    /** @test */
    public function error_is_thrown_if_trying_to_find_all_the_favicons_for_an_invalid_url(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new HttpDriver())->fetchAll('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }

    /** @test */
    public function error_is_thrown_if_no_icons_can_be_found_for_a_url_and_the_throw_on_not_found_flag_is_true(): void
    {
        $responseHtml = <<<'HTML'
            <html lang="en">
                <link rel="localization" href="branding/brand.ftl" />
            </html>
        HTML;

        Http::fake([
            'https://example.com' => Http::response($responseHtml),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new HttpDriver())
                ->throw()
                ->fetchAll('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
    }

    /** @test */
    public function all_favicon_for_a_url_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.example.com.collection',
            [
                [
                    'favicon_url' => 'url-goes-here',
                    'icon_size' => null,
                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
                ],
                [
                    'favicon_url' => 'url-goes-here-1',
                    'icon_size' => 100,
                    'icon_type' => Favicon::TYPE_ICON,
                ],
                [
                    'favicon_url' => 'url-goes-here-1.com',
                    'icon_size' => 192,
                    'icon_type' => Favicon::TYPE_APPLE_TOUCH_ICON,
                ],
            ],
            now()->addHour(),
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->fetchAll('https://example.com');

        self::assertCount(3, $favicons);

        self::assertSame('url-goes-here', $favicons->first()->getFaviconUrl());
        self::assertSame('url-goes-here-1', $favicons->skip(1)->first()->getFaviconUrl());
        self::assertSame('url-goes-here-1.com', $favicons->skip(2)->first()->getFaviconUrl());

        self::assertNull($favicons->first()->getIconSize());
        self::assertSame(100, $favicons->skip(1)->first()->getIconSize());
        self::assertSame(192, $favicons->skip(2)->first()->getIconSize());

        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicons->first()->getIconType());
        self::assertSame(Favicon::TYPE_ICON, $favicons->skip(1)->first()->getIconType());
        self::assertSame(Favicon::TYPE_APPLE_TOUCH_ICON, $favicons->skip(2)->first()->getIconType());
    }

    /** @test */
    public function all_favicons_for_a_url_are_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.example.com.collection',
            [
                [
                    'favicon_url' => 'url-goes-here',
                    'icon_size' => null,
                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
                ],
                [
                    'favicon_url' => 'url-goes-here-1',
                    'icon_size' => 100,
                    'icon_type' => Favicon::TYPE_ICON,
                ],
                [
                    'favicon_url' => 'url-goes-here-1.com',
                    'icon_size' => 192,
                    'icon_type' => Favicon::TYPE_APPLE_TOUCH_ICON,
                ],
            ],
            now()->addHour(),
        );

        Http::fake([
            'https://example.com' => Http::response('<link href="/icon/favicon.ico" rel="icon">'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->useCache(false)->fetchAll('https://example.com');

        self::assertCount(1, $favicons);

        self::assertSame('https://example.com/icon/favicon.ico', $favicons->first()->getFaviconUrl());
    }

    /** @test */
    public function favicons_can_be_returned_using_the_fetchAllOr_method(): void
    {
        Http::fake([
            'https://example.com' => Http::response(self::htmlOptionOne()),
            '*' => Http::response('should not hit here'),
        ]);

        $favicons = (new HttpDriver())->fetchAllOr('https://example.com', 'should not fallback to this');

        self::assertCount(1, $favicons);

        self::assertSame('https://example.com/icon/is/here.ico', $favicons->first()->getFaviconUrl());
        self::assertSame(Favicon::TYPE_ICON, $favicons->first()->getIconType());
        self::assertSame(null, $favicons->first()->getIconSize());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchAllOr_method(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())
            ->useCache(true)
            ->fetchAllOr('https://example.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchAllOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://example.com/*' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new HttpDriver())
            ->fetchAllOr('https://example.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function can_set_the_user_agent_when_fetching()
    {
        Http::fake();

        $driver = new HttpDriver();

        // No user agent set.
        $driver->fetch('https://example.com');

        Http::assertSent(function (Request $request) {
            return $request->hasHeader('User-Agent', 'GuzzleHttp/7');
        });

        // Custom user agent.
        config()->set('favicon-fetcher.user_agent', 'test-user-agent');

        $driver->fetch('https://example.com');

        Http::assertSent(function (Request $request) {
            return $request->hasHeader('User-Agent', 'test-user-agent');
        });
    }

    /** @test */
    public function null_is_returned_if_using_fetch_and_the_link_has_no_href(): void
    {
        $responseHtml = <<<'HTML'
            <head>
                <meta charset="utf-8" />
                <title>Dummy title</title>
                <meta name="title" content="Dummy title">
                <meta name="description" content="Dummy title">
                <link rel="icon" type="image/x-icon">
            </head>
        HTML;

        Http::preventStrayRequests();

        Http::fake([
            'https://example.com' => Http::response($responseHtml),
            'https://example.com/favicon.ico' => Http::response(status: 404),
        ]);

        $favicon = (new HttpDriver())->fetch('https://example.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function null_is_returned_if_using_fetchAll_and_the_link_has_no_href(): void
    {
        $responseHtml = <<<'HTML'
            <head>
                <meta charset="utf-8" />
                <title>Dummy title</title>
                <meta name="title" content="Dummy title">
                <meta name="description" content="Dummy title">
                <link rel="icon" type="image/x-icon">
            </head>
        HTML;

        Http::preventStrayRequests();

        Http::fake([
            'https://example.com' => Http::response($responseHtml),
            'https://example.com/favicon.ico' => Http::response(status: 404),
        ]);

        $favicons = (new HttpDriver())->fetchAll('https://example.com');

        self::assertCount(0, $favicons);
    }

    public static function allFaviconLinksInHtmlProvider(): array
    {
        return [
            [
                self::htmlOptionOne(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_ICON),
                ]),
            ],
            [
                self::htmlOptionTwo(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_ICON),
                ]),
            ],
            [
                self::htmlOptionThree(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
                ]),
            ],
            [
                self::htmlOptionFour(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-57x57.png'))->setIconSize(57)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-60x60.png'))->setIconSize(60)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(72)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(76)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-76x76.png'))->setIconSize(114)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-120x120.png'))->setIconSize(120)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-144x144.png'))->setIconSize(144)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-152x152.png'))->setIconSize(152)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/favicon/android-icon-192x192.png'))->setIconSize(192)->setIconType(Favicon::TYPE_ICON),
                ]),
            ],
            [
                self::htmlOptionFive(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
                ]),
            ],
            [
                self::htmlOptionSix(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
                ]),
            ],
            [
                self::htmlOptionSeven(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
                ]),
            ],
            [
                self::htmlOptionEight(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_ICON),
                ]),
            ],
            [
                self::htmlOptionNine(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_ICON),
                ]),
            ],
            [
                self::htmlOptionTen(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://www.example.com/favicon123.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
                    (new Favicon('https://example.com', 'https://www.example.com/favicon123.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
                ]),
            ],
            [
                self::htmlOptionEleven(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-57x57.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(57),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-60x60.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(60),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-72x72.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(72),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-76x76.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(76),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-114x114.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(114),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-120x120.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(120),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-144x144.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(144),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-152x152.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(152),
                    (new Favicon('https://example.com', 'https://example.com/apple-icon-200x200.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(200),
                    (new Favicon('https://example.com', 'https://example.com/android-icon-192x192.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(192),
                    (new Favicon('https://example.com', 'https://example.com/favicon-32x32.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(32),
                    (new Favicon('https://example.com', 'https://example.com/favicon-96x96.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(96),
                ]),
            ],
            [
                self::htmlOptionThirteen(),
                FaviconCollection::make([
                    (new Favicon('https://example.com', 'https://example.com/favicon-96x96.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(96),
                ]),
            ],
        ];
    }

    public static function faviconLinksInHtmlProvider(): array
    {
        return [
            [self::htmlOptionOne(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_ICON],
            [self::htmlOptionTwo(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_ICON],
            [self::htmlOptionThree(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_SHORTCUT_ICON],
            [self::htmlOptionFour(), 'https://example.com/favicon/favicon-32x32.png', null, Favicon::TYPE_ICON],
            [self::htmlOptionFive(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_SHORTCUT_ICON],
            [self::htmlOptionSix(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_SHORTCUT_ICON],
            [self::htmlOptionSeven(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_SHORTCUT_ICON],
            [self::htmlOptionEight(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_ICON],
            [self::htmlOptionNine(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_ICON],
            [self::htmlOptionTen(), 'https://www.example.com/favicon123.ico', null, Favicon::TYPE_SHORTCUT_ICON],
            [self::htmlOptionEleven(), 'https://example.com/android-icon-192x192.png', 192, Favicon::TYPE_ICON],
            [self::htmlOptionTwelve(), 'https://example.com/android-icon-192x192.png', 192, Favicon::TYPE_ICON],
            [self::htmlOptionThirteen(), 'https://example.com/favicon-96x96.png', 96, Favicon::TYPE_ICON],
        ];
    }

    private static function htmlOptionOne(): string
    {
        return <<<'HTML'
            <html lang="en">
                <link rel="icon" href="icon/is/here.ico" />
            </html>
        HTML;
    }

    private static function htmlOptionTwo(): string
    {
        return <<<'HTML'
            <html lang="en">
                <link rel="icon" href="/icon/is/here.ico" />
            </html>
        HTML;
    }

    private static function htmlOptionThree(): string
    {
        return <<<'HTML'
            <html lang="en">
                <link rel="shortcut icon" href="/icon/is/here.ico" />
            </html>
        HTML;
    }

    private static function htmlOptionFour(): string
    {
        return <<<'HTML'
            <html lang="en">
                <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"/>
            </html>
        HTML;
    }

    private static function htmlOptionFive(): string
    {
        return <<<'HTML'
            <html lang="en">
                <link href="/icon/is/here.ico" rel="shortcut icon" />
            </html>
        HTML;
    }

    private static function htmlOptionSix(): string
    {
        return <<<'HTML'
            <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" >
        HTML;
    }

    private static function htmlOptionSeven(): string
    {
        return <<<'HTML'
            <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" ></script>
            </head>
        HTML;
    }

    private static function htmlOptionEight(): string
    {
        return <<<'HTML'
            <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" >
        HTML;
    }

    private static function htmlOptionNine(): string
    {
        return <<<'HTML'
            <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" ></script>
            </head>
        HTML;
    }

    private static function htmlOptionTen(): string
    {
        return <<<'HTML'
            <head>
                <title>Test Title</title>
                <meta content='IE=edge' http-equiv='X-UA-Compatible'>
                <meta content='telephone=no' name='format-detection'>
                <meta content='width=device-width, initial-scale=1, maximum-scale=1' name='viewport'>
                <link href='https://www.example.com/favicon123.png' rel='apple-touch-icon'>
                <link href='https://www.example.com/favicon123.ico' rel='shortcut icon' type='image/x-icon'>
            </head>
        HTML;
    }

    private static function htmlOptionEleven(): string
    {
        return <<<'HTML'
            <head>
                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
                <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
                <link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
                <link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
                <link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
                <link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
                <link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
                <link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
                <link rel="apple-touch-icon" sizes="200x200" href="/apple-icon-200x200.png">
                <link rel="icon" type="image/png" sizes="192x192"  href="/android-icon-192x192.png">
                <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
                <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
                <link rel="manifest" href="/manifest.json">
                <meta name="msapplication-TileColor" content="#ffffff">
                <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
                <meta name="theme-color" content="#ffffff">
                <title>Dummy title</title>
            </head>
        HTML;
    }

    private static function htmlOptionTwelve(): string
    {
        return <<<'HTML'
            <head>
                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <link rel="apple-touch-icon" sizes="57x57" href=/apple-icon-57x57.png>
                <link rel=apple-touch-icon sizes=60x60 href=/apple-icon-60x60.png>
                <link rel="apple-touch-icon" sizes="72x72" href=/apple-icon-72x72.png>
                <link rel="apple-touch-icon" sizes="76x76" href=/apple-icon-76x76.png>
                <link rel="apple-touch-icon" sizes="114x114" href=/apple-icon-114x114.png>
                <link rel="apple-touch-icon" sizes="120x120" href=/apple-icon-120x120.png>
                <link rel="apple-touch-icon" sizes="144x144" href=/apple-icon-144x144.png>
                <link rel="apple-touch-icon" sizes="152x152" href=/apple-icon-152x152.png>
                <link rel="apple-touch-icon" sizes="200x200" href=/apple-icon-200x200.png>
                <link rel="icon" type="image/png" sizes="192x192"  href=/android-icon-192x192.png>
                <link rel="icon" type="image/png" sizes="32x32" href=/favicon-32x32.png>
                <link rel="icon" type="image/png" sizes="96x96" href=/favicon-96x96.png>
                <link rel="manifest" href="/manifest.json">
                <meta name="msapplication-TileColor" content="#ffffff">
                <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
                <meta name="theme-color" content="#ffffff">
                <title>Dummy title</title>
            </head>
        HTML;
    }

    private static function htmlOptionThirteen(): string
    {
        return <<<'HTML'
            <head>
                <meta charset="utf-8" />
                <title>Dummy title</title>
                <meta name="title" content="Dummy title">
                <meta name="description" content="Dummy title">
                <link rel="icon" type="image/x-icon">
                <link rel="icon" type="image/png" sizes="96x96" href=/favicon-96x96.png>
            </head>
        HTML;
    }
}


================================================
FILE: tests/Feature/Drivers/UnavatarDriverTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\Drivers;

use AshAllenDesign\FaviconFetcher\Drivers\UnavatarDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconNotFoundException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidUrlException;
use AshAllenDesign\FaviconFetcher\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\NullDriver;
use AshAllenDesign\FaviconFetcher\Tests\Feature\TestCase;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class UnavatarDriverTest extends TestCase
{
    use LazilyRefreshDatabase;

    /**
     * @test
     *
     * @testWith ["https"]
     *           ["http"]
     */
    public function favicon_can_be_fetched_from_driver(string $protocol): void
    {
        Http::fake([
            'https://unavatar.io/example.com' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new UnavatarDriver())->fetch($protocol.'://example.com');

        self::assertSame('https://unavatar.io/example.com?fallback=false', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
    {
        Cache::put(
            'favicon-fetcher.example.com',
            [
                'favicon_url' => 'url-goes-here',
                'icon_size' => null,
                'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
            ],
            now()->addHour()
        );

        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new UnavatarDriver())->fetch('https://example.com');

        self::assertSame('url-goes-here', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
    {
        Cache::put(
            'favicon-fetcher.https://example.com',
            'url-goes-here',
            now()->addHour()
        );

        Http::fake([
            'https://unavatar.io/example.com' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new UnavatarDriver())->useCache(false)->fetch('https://example.com');

        self::assertSame('https://unavatar.io/example.com?fallback=false', $favicon->getFaviconUrl());
    }

    /** @test */
    public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new UnavatarDriver())->useCache(true)->fetch('https://example.com');

        self::assertNull($favicon);
    }

    /** @test */
    public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
    {
        Http::fake([
            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new CustomDriver());

        $favicon = (new UnavatarDriver())
            ->withFallback('custom-driver')
            ->useCache(true)
            ->fetch('https://example.com');

        self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
    {
        Http::fake([
            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new UnavatarDriver())
                ->throw()
                ->useCache(true)
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method(): void
    {
        Http::fake([
            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new UnavatarDriver())
            ->useCache(true)
            ->fetchOr('https://example.com', 'fallback-to-this');

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
    {
        Http::fake([
            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = (new UnavatarDriver())
            ->fetchOr('https://example.com', function () {
                return 'fallback-to-this';
            });

        self::assertSame('fallback-to-this', $favicon);
    }

    /** @test */
    public function exception_can_be_thrown_after_attempting_a_fallback(): void
    {
        Http::fake([
            'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
            '*' => Http::response('should not hit here'),
        ]);

        FetcherManager::extend('custom-driver', new NullDriver());

        $exception = null;

        try {
            (new UnavatarDriver())
                ->throw()
                ->withFallback('custom-driver')
                ->fetch('https://example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(FaviconNotFoundException::class, $exception);
        self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());

        self::assertTrue(NullDriver::$flag);
    }

    /** @test */
    public function exception_is_thrown_if_the_url_is_invalid(): void
    {
        Http::fake([
            '*' => Http::response('should not hit here'),
        ]);

        $exception = null;

        try {
            (new UnavatarDriver())->fetch('example.com');
        } catch (\Exception $e) {
            $exception = $e;
        }

        self::assertInstanceOf(InvalidUrlException::class, $exception);
        self::assertSame('example.com is not a valid URL', $exception->getMessage());
    }
}


================================================
FILE: tests/Feature/FaviconTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature;

use AshAllenDesign\FaviconFetcher\Exceptions\InvalidIconSizeException;
use AshAllenDesign\FaviconFetcher\Exceptions\InvalidIconTypeException;
use AshAllenDesign\FaviconFetcher\Favicon;
use Carbon\CarbonInterface;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Mockery;

class FaviconTest extends TestCase
{
    use LazilyRefreshDatabase;

    /** @test */
    public function favicon_url_can_be_returned(): void
    {
        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        );

        self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());
    }

    /** @test */
    public function favicon_contents_can_be_returned(): void
    {
        Http::fake([
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        );

        self::assertSame('favicon contents here', $favicon->content());
    }

    /** @test */
    public function url_can_be_returned(): void
    {
        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        );

        self::assertSame('https://example.com', $favicon->getUrl());
    }

    /** @test */
    public function retrieved_from_cache_value_can_be_returned_if_the_favicon_was_retrieved_from_the_cache(): void
    {
        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
            retrievedFromCache: true,
        );

        self::assertTrue($favicon->retrievedFromCache());
    }

    /** @test */
    public function retrieved_from_cache_value_can_be_returned_if_the_favicon_was_not_retrieved_from_the_cache(): void
    {
        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        );

        self::assertFalse($favicon->retrievedFromCache());
    }

    /** @test */
    public function favicon_can_be_cached_if_it_is_not_already_cached(): void
    {
        Carbon::setTestNow(now());

        $expectedTtl = now()->addMinute();

        Cache::shouldReceive('put')
            ->withArgs([
                'favicon-fetcher.example.com',
                [
                    'favicon_url' => 'https://example.com/favicon.ico',
                    'icon_size' => null,
                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
                ],
                Mockery::on(fn (CarbonInterface $ttl): bool => $ttl->eq($expectedTtl)),
            ])
            ->once();

        (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))->cache($expectedTtl);
    }

    /** @test */
    public function favicon_cannot_be_cached_if_it_is_already_cached(): void
    {
        Carbon::setTestNow(now());

        $expectedTtl = now()->addMinute();

        Cache::shouldReceive('put')->never();

        (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
            retrievedFromCache: true,
        ))->cache($expectedTtl);
    }

    /** @test */
    public function favicon_can_be_cached_if_it_is_already_cached_and_the_force_flag_is_passed(): void
    {
        Carbon::setTestNow(now());

        $expectedTtl = now()->addMinute();

        Cache::shouldReceive('put')
            ->withArgs([
                'favicon-fetcher.example.com',
                [
                    'favicon_url' => 'https://example.com/favicon.ico',
                    'icon_size' => null,
                    'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
                ],
                Mockery::on(fn (CarbonInterface $ttl): bool => $ttl->eq($expectedTtl)),
            ])
            ->once();

        (new Favicon(
            'https://example.com',
            'https://example.com/favicon.ico',
        ))->cache(now()->addMinute(), true);
    }

    /** @test */
    public function favicon_contents_be_stored(): void
    {
        Storage::fake();

        Http::fake([
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        );

        $path = $favicon->store('favicons');

        self::assertSame('favicon contents here', Storage::get($path));
    }

    /** @test */
    public function favicon_contents_be_stored_if_the_favicon_url_does_not_have_an_image_extension(): void
    {
        Storage::fake();

        Http::fake([
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            'https://example.com/favicon.com' => Http::response(body: 'favicon contents here', headers: ['content-type' => 'image/png']),
            '*' => Http::response('should not hit here'),
        ]);

        $favicon = new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.com',
        );

        $path = $favicon->store('favicons');

        self::assertSame('favicon contents here', Storage::get($path));
        self::assertTrue(Str::of($path)->endsWith('.png'));
    }

    /** @test */
    public function favicon_contents_can_be_stored_with_a_custom_file_name(): void
    {
        Storage::fake();

        Http::fake([
            'https://example.com/favicon.ico' => Http::response('favicon contents here'),
            '*' => Http::response('should not hit here'),
        ]);

        (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))->storeAs('favicons', 'fetched');

        self::assertSame('favicon contents here', Storage::get('favicons/fetched.ico'));
    }

    public function icon_type_defaults_to_unknown_if_not_explicitly_set(): void
    {
        $iconType = (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))->getIconType();

        self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $iconType);
    }

    /**
     * @test
     *
     * @dataProvider iconTypeProvider
     */
    public function icon_type_can_be_set_and_returned(string $expectedIconType): void
    {
        $iconType = (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))
            ->setIconType($expectedIconType)
            ->getIconType();

        self::assertSame($expectedIconType, $iconType);
    }

    /**
     * @test
     *
     * @dataProvider iconSizeProvider
     */
    public function icon_size_can_be_set_and_returned(?int $expectedIconSize): void
    {
        $iconSize = (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))
            ->setIconSize($expectedIconSize)
            ->getIconSize();

        self::assertSame($expectedIconSize, $iconSize);
    }

    /** @test */
    public function exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_type(): void
    {
        $this->expectException(InvalidIconTypeException::class);
        $this->expectExceptionMessage('The type [INVALID] is not a valid favicon type.');

        (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))->setIconType('INVALID');
    }

    /** @test */
    public function exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_size(): void
    {
        $this->expectException(InvalidIconSizeException::class);
        $this->expectExceptionMessage('The size [-1] is not a valid favicon size.');

        (new Favicon(
            url: 'https://example.com',
            faviconUrl: 'https://example.com/favicon.ico',
        ))->setIconSize(-1);
    }

    public static function iconTypeProvider(): array
    {
        return [
            [Favicon::TYPE_ICON],
            [Favicon::TYPE_SHORTCUT_ICON],
            [Favicon::TYPE_APPLE_TOUCH_ICON],
            [Favicon::TYPE_ICON_UNKNOWN],
        ];
    }

    public static function iconSizeProvider(): array
    {
        return [
            [null],
            [16],
            [190],
        ];
    }
}


================================================
FILE: tests/Feature/FetcherManagerTest.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature;

use AshAllenDesign\FaviconFetcher\Drivers\DuckDuckGoDriver;
use AshAllenDesign\FaviconFetcher\Drivers\FaviconGrabberDriver;
use AshAllenDesign\FaviconFetcher\Drivers\FaviconKitDriver;
use AshAllenDesign\FaviconFetcher\Drivers\GoogleSharedStuffDriver;
use AshAllenDesign\FaviconFetcher\Drivers\HttpDriver;
use AshAllenDesign\FaviconFetcher\Exceptions\FaviconFetcherException;
use AshAllenDesign\FaviconFetcher\Facades\Favicon;
use AshAllenDesign\FaviconFetcher\FetcherManager;
use AshAllenDesign\FaviconFetcher\Tests\Feature\_data\CustomDriver;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Mockery;

class FetcherManagerTest extends TestCase
{
    use LazilyRefreshDatabase;

    /** @test */
    public function default_driver_can_be_returned(): void
    {
        config(['favicon-fetcher.default' => 'http']);

        self::assertInstanceOf(HttpDriver::class, FetcherManager::driver());
    }

    /** @test */
    public function http_driver_can_be_returned(): void
    {
        self::assertInstanceOf(HttpDriver::class, FetcherManager::driver('http'));
    }

    /** @test */
    public function google_shared_stuff_driver_can_be_returned(): void
    {
        self::assertInstanceOf(GoogleSharedStuffDriver::class, FetcherManager::driver('google-shared-stuff'));
    }

    /** @test */
    public function favicon_kit_driver_can_be_returned(): void
    {
        self::assertInstanceOf(FaviconKitDriver::class, FetcherManager::driver('favicon-kit'));
    }

    /** @test */
    public function favicon_grabber_driver_can_be_returned(): void
    {
        self::assertInstanceOf(FaviconGrabberDriver::class, FetcherManager::driver('favicon-grabber'));
    }

    /** @test */
    public function duck_duck_go_driver_can_be_returned(): void
    {
        self::assertInstanceOf(DuckDuckGoDriver::class, FetcherManager::driver('duck-duck-go'));
    }

    /** @test */
    public function custom_driver_can_be_returned(): void
    {
        FetcherManager::extend('custom-driver', new CustomDriver());
        self::assertInstanceOf(CustomDriver::class, FetcherManager::driver('custom-driver'));
    }

    /** @test */
    public function exception_is_thrown_if_the_driver_is_invalid(): void
    {
        $this->expectException(FaviconFetcherException::class);
        $this->expectExceptionMessage('invalid is not a valid driver');

        FetcherManager::driver('invalid');
    }

    /** @test */
    public function method_calls_to_the_manager_are_forwarded_to_the_driver(): void
    {
        $mock = tap(
            Mockery::mock(CustomDriver::class),
            function (Mockery\MockInterface $mock): void {
                $mock->shouldReceive('fetch')
                    ->once()
                    ->withArgs(['https://example.com']);
            }
        );

        FetcherManager::extend('custom-driver', $mock);

        config(['favicon-fetcher.default' => 'custom-driver']);

        (new FetcherManager())->fetch('https://example.com');
    }

    /** @test */
    public function method_calls_to_the_manager_are_forwarded_to_the_driver_using_the_facade(): void
    {
        $mock = tap(
            Mockery::mock(CustomDriver::class),
            function (Mockery\MockInterface $mock): void {
                $mock->shouldReceive('fetch')
                    ->once()
                    ->withArgs(['https://example.com']);
            }
        );

        FetcherManager::extend('custom-driver', $mock);

        config(['favicon-fetcher.default' => 'custom-driver']);

        Favicon::fetch('https://example.com');
    }

    /** @test */
    public function driver_can_be_returned_using_the_facade(): void
    {
        self::assertInstanceOf(FaviconKitDriver::class, Favicon::driver('favicon-kit'));
    }
}


================================================
FILE: tests/Feature/TestCase.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature;

use AshAllenDesign\FaviconFetcher\FaviconFetcherProvider;
use Orchestra\Testbench\TestCase as OrchestraTestCase;

abstract class TestCase extends OrchestraTestCase
{
    /**
     * Load package service provider.
     *
     * @param  $app
     * @return array
     */
    protected function getPackageProviders($app)
    {
        return [FaviconFetcherProvider::class];
    }
}


================================================
FILE: tests/Feature/_data/CustomDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\_data;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Favicon;

class CustomDriver implements Fetcher
{
    public function fetch(string $url): ?Favicon
    {
        return new Favicon(
            url: 'url-from-default',
            faviconUrl: 'favicon-from-default',
            fromDriver: $this,
        );
    }

    public function fetchOr(string $url, mixed $default): mixed
    {
        return 'default';
    }

    public function fetchAll(string $url): FaviconCollection
    {
        // Implement this method if needed for testing.
    }

    public function fetchAllOr(string $url, mixed $default): mixed
    {
        // Implement this method if needed for testing.
    }
}


================================================
FILE: tests/Feature/_data/NullDriver.php
================================================
<?php

declare(strict_types=1);

namespace AshAllenDesign\FaviconFetcher\Tests\Feature\_data;

use AshAllenDesign\FaviconFetcher\Collections\FaviconCollection;
use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
use AshAllenDesign\FaviconFetcher\Favicon;

class NullDriver implements Fetcher
{
    public static bool $flag = false;

    public function fetch(string $url): ?Favicon
    {
        static::$flag = true;

        return null;
    }

    public function fetchOr(string $url, mixed $default): mixed
    {
        return 'default';
    }

    public function fetchAll(string $url): FaviconCollection
    {
        // Implement this method if needed for testing.
    }

    public function fetchAllOr(string $url, mixed $default): mixed
    {
        // Implement this method if needed for testing.
    }
}
Download .txt
gitextract_e3mdaezn/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci-phpstan.yml
│       └── ci-tests.yml
├── .gitignore
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer.json
├── config/
│   └── favicon-fetcher.php
├── phpstan.neon
├── phpunit.xml
├── src/
│   ├── Collections/
│   │   └── FaviconCollection.php
│   ├── Concerns/
│   │   ├── BuildsCacheKeys.php
│   │   ├── HasDefaultFunctionality.php
│   │   ├── MakesHttpRequests.php
│   │   └── ValidatesUrls.php
│   ├── Contracts/
│   │   └── Fetcher.php
│   ├── Drivers/
│   │   ├── DuckDuckGoDriver.php
│   │   ├── FaviconGrabberDriver.php
│   │   ├── FaviconKitDriver.php
│   │   ├── GoogleSharedStuffDriver.php
│   │   ├── HttpDriver.php
│   │   └── UnavatarDriver.php
│   ├── Exceptions/
│   │   ├── ConnectionException.php
│   │   ├── FaviconFetcherException.php
│   │   ├── FaviconNotFoundException.php
│   │   ├── FeatureNotSupportedException.php
│   │   ├── InvalidIconSizeException.php
│   │   ├── InvalidIconTypeException.php
│   │   └── InvalidUrlException.php
│   ├── Facades/
│   │   └── Favicon.php
│   ├── Favicon.php
│   ├── FaviconFetcherProvider.php
│   └── FetcherManager.php
└── tests/
    └── Feature/
        ├── Collections/
        │   └── FaviconCollectionTest.php
        ├── Concerns/
        │   └── MakesHttpRequests/
        │       ├── HttpClientTest.php
        │       └── WithRequestExceptionHandlingTest.php
        ├── Drivers/
        │   ├── DuckDuckGoDriverTest.php
        │   ├── FaviconGrabberDriverTest.php
        │   ├── FaviconKitDriverTest.php
        │   ├── GoogleSharedStuffDriverTest.php
        │   ├── HttpDriverTest.php
        │   └── UnavatarDriverTest.php
        ├── FaviconTest.php
        ├── FetcherManagerTest.php
        ├── TestCase.php
        └── _data/
            ├── CustomDriver.php
            └── NullDriver.php
Download .txt
SYMBOL INDEX (250 symbols across 37 files)

FILE: src/Collections/FaviconCollection.php
  class FaviconCollection (line 16) | class FaviconCollection extends Collection
    method makeFromCache (line 29) | public static function makeFromCache(array $items = []): static
    method cache (line 44) | public function cache(CarbonInterface $ttl, bool $force = false): self
    method largest (line 64) | public function largest(): ?Favicon
    method largestByFileSize (line 71) | public function largestByFileSize(): ?Favicon

FILE: src/Concerns/BuildsCacheKeys.php
  type BuildsCacheKeys (line 7) | trait BuildsCacheKeys
    method buildCacheKey (line 15) | public function buildCacheKey(string $url): string
    method buildCacheKeyForCollection (line 28) | public function buildCacheKeyForCollection(string $url): string

FILE: src/Concerns/HasDefaultFunctionality.php
  type HasDefaultFunctionality (line 16) | trait HasDefaultFunctionality
    method fetchOr (line 53) | public function fetchOr(string $url, mixed $default): mixed
    method fetchAllOr (line 74) | public function fetchAllOr(string $url, mixed $default): mixed
    method throw (line 91) | public function throw(bool $throw = true): self
    method withFallback (line 105) | public function withFallback(string ...$fallbacks): self
    method useCache (line 118) | public function useCache(bool $useCache = true): self
    method notFound (line 136) | protected function notFound(string $url)
    method attemptFallbacks (line 155) | protected function attemptFallbacks(string $url): ?FetchedFavicon
    method attemptToFetchFromCache (line 174) | protected function attemptToFetchFromCache(string $url): ?FetchedFavicon
    method attemptToFetchCollectionFromCache (line 211) | protected function attemptToFetchCollectionFromCache(string $url): ?Fa...

FILE: src/Concerns/MakesHttpRequests.php
  type MakesHttpRequests (line 12) | trait MakesHttpRequests
    method httpClient (line 14) | protected function httpClient(): PendingRequest
    method withRequestExceptionHandling (line 30) | protected function withRequestExceptionHandling(\Closure $callback): m...

FILE: src/Concerns/ValidatesUrls.php
  type ValidatesUrls (line 7) | trait ValidatesUrls
    method urlIsValid (line 15) | protected function urlIsValid(string $url): bool

FILE: src/Contracts/Fetcher.php
  type Fetcher (line 11) | interface Fetcher
    method fetch (line 19) | public function fetch(string $url): ?Favicon;
    method fetchAll (line 27) | public function fetchAll(string $url): FaviconCollection;
    method fetchOr (line 40) | public function fetchOr(string $url, mixed $default): mixed;
    method fetchAllOr (line 54) | public function fetchAllOr(string $url, mixed $default): mixed;

FILE: src/Drivers/DuckDuckGoDriver.php
  class DuckDuckGoDriver (line 20) | class DuckDuckGoDriver implements Fetcher
    method fetch (line 39) | public function fetch(string $url): ?Favicon
    method fetchAll (line 62) | public function fetchAll(string $url): FaviconCollection

FILE: src/Drivers/FaviconGrabberDriver.php
  class FaviconGrabberDriver (line 20) | class FaviconGrabberDriver implements Fetcher
    method fetch (line 39) | public function fetch(string $url): ?Favicon
    method fetchAll (line 66) | public function fetchAll(string $url): FaviconCollection

FILE: src/Drivers/FaviconKitDriver.php
  class FaviconKitDriver (line 19) | class FaviconKitDriver implements Fetcher
    method fetch (line 37) | public function fetch(string $url): ?Favicon
    method fetchAll (line 60) | public function fetchAll(string $url): FaviconCollection

FILE: src/Drivers/GoogleSharedStuffDriver.php
  class GoogleSharedStuffDriver (line 20) | class GoogleSharedStuffDriver implements Fetcher
    method fetch (line 39) | public function fetch(string $url): ?Favicon
    method fetchAll (line 60) | public function fetchAll(string $url): FaviconCollection

FILE: src/Drivers/HttpDriver.php
  class HttpDriver (line 22) | class HttpDriver implements Fetcher
    method fetch (line 40) | public function fetch(string $url): ?Favicon
    method fetchAll (line 60) | public function fetchAll(string $url): FaviconCollection
    method faviconUrlCanBeReached (line 102) | private function faviconUrlCanBeReached(string $faviconUrl): bool
    method attemptToResolveFromHeadTags (line 124) | private function attemptToResolveFromHeadTags(string $url): ?Favicon
    method attemptToResolveAllFromHeadTags (line 165) | private function attemptToResolveAllFromHeadTags(string $url): ?Favico...
    method guessTypeFromElement (line 207) | private function guessTypeFromElement(Crawler $linkElement): string
    method convertToAbsoluteUrl (line 224) | private function convertToAbsoluteUrl(string $baseUrl, string $favicon...
    method guessDefaultUrl (line 242) | private function guessDefaultUrl(string $url): string
    method stripPathFromUrl (line 254) | private function stripPathFromUrl(string $url): string

FILE: src/Drivers/UnavatarDriver.php
  class UnavatarDriver (line 19) | class UnavatarDriver implements Fetcher
    method fetch (line 37) | public function fetch(string $url): ?Favicon
    method fetchAll (line 60) | public function fetchAll(string $url): FaviconCollection

FILE: src/Exceptions/ConnectionException.php
  class ConnectionException (line 5) | class ConnectionException extends FaviconFetcherException

FILE: src/Exceptions/FaviconFetcherException.php
  class FaviconFetcherException (line 9) | class FaviconFetcherException extends Exception

FILE: src/Exceptions/FaviconNotFoundException.php
  class FaviconNotFoundException (line 7) | class FaviconNotFoundException extends FaviconFetcherException

FILE: src/Exceptions/FeatureNotSupportedException.php
  class FeatureNotSupportedException (line 7) | class FeatureNotSupportedException extends FaviconFetcherException

FILE: src/Exceptions/InvalidIconSizeException.php
  class InvalidIconSizeException (line 7) | class InvalidIconSizeException extends FaviconFetcherException

FILE: src/Exceptions/InvalidIconTypeException.php
  class InvalidIconTypeException (line 7) | class InvalidIconTypeException extends FaviconFetcherException

FILE: src/Exceptions/InvalidUrlException.php
  class InvalidUrlException (line 7) | class InvalidUrlException extends FaviconFetcherException

FILE: src/Facades/Favicon.php
  class Favicon (line 26) | class Favicon extends Facade
    method getFacadeAccessor (line 33) | protected static function getFacadeAccessor(): string

FILE: src/Favicon.php
  class Favicon (line 20) | class Favicon
    method __construct (line 66) | public function __construct(
    method setIconSize (line 78) | public function setIconSize(?int $size): static
    method setIconType (line 89) | public function setIconType(string $type): static
    method getUrl (line 100) | public function getUrl(): string
    method getFaviconUrl (line 105) | public function getFaviconUrl(): string
    method retrievedFromCache (line 110) | public function retrievedFromCache(): bool
    method content (line 122) | public function content(): string
    method cache (line 137) | public function cache(CarbonInterface $ttl, bool $force = false): self
    method store (line 157) | public function store(string $directory, ?string $disk = null): string
    method storeAs (line 170) | public function storeAs(string $directory, string $filename, ?string $...
    method getIconType (line 179) | public function getIconType(): string
    method getIconSize (line 184) | public function getIconSize(): ?int
    method buildStoragePath (line 189) | protected function buildStoragePath(string $directory, string $filenam...
    method guessFileExtension (line 199) | protected function guessFileExtension(): string
    method guessFileExtensionFromMimeType (line 213) | protected function guessFileExtensionFromMimeType(): ?string
    method acceptableIconType (line 233) | private function acceptableIconType(string $type): bool
    method toCache (line 251) | public function toCache(): array

FILE: src/FaviconFetcherProvider.php
  class FaviconFetcherProvider (line 9) | class FaviconFetcherProvider extends ServiceProvider
    method register (line 16) | public function register(): void
    method boot (line 28) | public function boot(): void

FILE: src/FetcherManager.php
  class FetcherManager (line 16) | class FetcherManager
    method driver (line 23) | public static function driver(?string $driver = null): Fetcher
    method extend (line 38) | public static function extend(string $name, Fetcher $fetcher): void
    method attemptToCreateCustomDriver (line 43) | protected static function attemptToCreateCustomDriver(string $driver):...
    method __call (line 49) | public function __call(string $method, mixed $parameters): mixed

FILE: tests/Feature/Collections/FaviconCollectionTest.php
  class FaviconCollectionTest (line 13) | final class FaviconCollectionTest extends TestCase
    method favicon_collection_can_be_cached_if_the_collection_was_not_retrieved_from_the_cache (line 18) | public function favicon_collection_can_be_cached_if_the_collection_was...
    method favicon_collection_can_be_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_true (line 47) | public function favicon_collection_can_be_cached_if_the_collection_was...
    method favicon_collection_is_not_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_false (line 79) | public function favicon_collection_is_not_cached_if_the_collection_was...
    method favicon_collection_is_not_cached_if_the_collection_is_empty (line 100) | public function favicon_collection_is_not_cached_if_the_collection_is_...
    method largest_favicon_can_be_retrieved (line 110) | public function largest_favicon_can_be_retrieved(): void
    method largest_favicon_can_be_retrieved_if_there_are_only_null_sizes (line 130) | public function largest_favicon_can_be_retrieved_if_there_are_only_nul...
    method largest_favicon_can_be_retrieved_based_on_file_size (line 141) | public function largest_favicon_can_be_retrieved_based_on_file_size()

FILE: tests/Feature/Concerns/MakesHttpRequests/HttpClientTest.php
  class HttpClientTest (line 10) | final class HttpClientTest extends TestCase
    method http_client_is_returned_with_correct_options (line 15) | public function http_client_is_returned_with_correct_options(): void
    method http_client_is_returned_with_correct_verify_tls_option (line 31) | public function http_client_is_returned_with_correct_verify_tls_option...

FILE: tests/Feature/Concerns/MakesHttpRequests/WithRequestExceptionHandlingTest.php
  class WithRequestExceptionHandlingTest (line 12) | final class WithRequestExceptionHandlingTest extends TestCase
    method exception_is_handled_and_rethrown (line 17) | public function exception_is_handled_and_rethrown(): void

FILE: tests/Feature/Drivers/DuckDuckGoDriverTest.php
  class DuckDuckGoDriverTest (line 19) | class DuckDuckGoDriverTest extends TestCase
    method favicon_can_be_fetched_from_driver (line 29) | public function favicon_can_be_fetched_from_driver(string $protocol): ...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists (line 42) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 64) | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but...
    method null_is_returned_if_the_driver_cannot_find_the_favicon (line 83) | public function null_is_returned_if_the_driver_cannot_find_the_favicon...
    method fallback_is_attempted_if_the_driver_cannot_find_the_favicon (line 96) | public function fallback_is_attempted_if_the_driver_cannot_find_the_fa...
    method exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true (line 114) | public function exception_is_thrown_if_the_driver_cannot_find_the_favi...
    method default_value_can_be_returned_using_fetchOr_method (line 137) | public function default_value_can_be_returned_using_fetchOr_method(): ...
    method default_value_can_be_returned_using_fetchOr_method_with_a_closure (line 152) | public function default_value_can_be_returned_using_fetchOr_method_wit...
    method exception_can_be_thrown_after_attempting_a_fallback (line 168) | public function exception_can_be_thrown_after_attempting_a_fallback():...
    method exception_is_thrown_if_the_url_is_invalid (line 195) | public function exception_is_thrown_if_the_url_is_invalid(): void

FILE: tests/Feature/Drivers/FaviconGrabberDriverTest.php
  class FaviconGrabberDriverTest (line 19) | class FaviconGrabberDriverTest extends TestCase
    method favicon_can_be_fetched_from_driver (line 29) | public function favicon_can_be_fetched_from_driver(string $protocol): ...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists (line 42) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 64) | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but...
    method null_is_returned_if_the_driver_cannot_find_the_favicon (line 83) | public function null_is_returned_if_the_driver_cannot_find_the_favicon...
    method null_is_returned_if_the_domain_is_invalid (line 96) | public function null_is_returned_if_the_domain_is_invalid(): void
    method fallback_is_attempted_if_the_driver_cannot_find_the_favicon (line 109) | public function fallback_is_attempted_if_the_driver_cannot_find_the_fa...
    method exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true (line 127) | public function exception_is_thrown_if_the_driver_cannot_find_the_favi...
    method default_value_can_be_returned_using_fetchOr_method (line 150) | public function default_value_can_be_returned_using_fetchOr_method(): ...
    method default_value_can_be_returned_using_fetchOr_method_with_a_closure (line 165) | public function default_value_can_be_returned_using_fetchOr_method_wit...
    method exception_can_be_thrown_after_attempting_a_fallback (line 181) | public function exception_can_be_thrown_after_attempting_a_fallback():...
    method exception_is_thrown_if_the_url_is_invalid (line 208) | public function exception_is_thrown_if_the_url_is_invalid(): void
    method successfulResponseBody (line 226) | private function successfulResponseBody(): array
    method successfulEmptyResponseBody (line 263) | private function successfulEmptyResponseBody(): array
    method domainNotFoundResponseBody (line 271) | private function domainNotFoundResponseBody(): array

FILE: tests/Feature/Drivers/FaviconKitDriverTest.php
  class FaviconKitDriverTest (line 19) | class FaviconKitDriverTest extends TestCase
    method favicon_can_be_fetched_from_driver (line 29) | public function favicon_can_be_fetched_from_driver(string $protocol): ...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists (line 42) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 64) | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but...
    method null_is_returned_if_the_driver_cannot_find_the_favicon (line 83) | public function null_is_returned_if_the_driver_cannot_find_the_favicon...
    method fallback_is_attempted_if_the_driver_cannot_find_the_favicon (line 96) | public function fallback_is_attempted_if_the_driver_cannot_find_the_fa...
    method exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true (line 114) | public function exception_is_thrown_if_the_driver_cannot_find_the_favi...
    method default_value_can_be_returned_using_fetchOr_method (line 137) | public function default_value_can_be_returned_using_fetchOr_method(): ...
    method default_value_can_be_returned_using_fetchOr_method_with_a_closure (line 152) | public function default_value_can_be_returned_using_fetchOr_method_wit...
    method exception_can_be_thrown_after_attempting_a_fallback (line 168) | public function exception_can_be_thrown_after_attempting_a_fallback():...
    method exception_is_thrown_if_the_url_is_invalid (line 195) | public function exception_is_thrown_if_the_url_is_invalid(): void

FILE: tests/Feature/Drivers/GoogleSharedStuffDriverTest.php
  class GoogleSharedStuffDriverTest (line 19) | class GoogleSharedStuffDriverTest extends TestCase
    method favicon_can_be_fetched_from_driver (line 29) | public function favicon_can_be_fetched_from_driver(string $protocol): ...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists (line 42) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 64) | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but...
    method null_is_returned_if_the_driver_cannot_find_the_favicon (line 83) | public function null_is_returned_if_the_driver_cannot_find_the_favicon...
    method fallback_is_attempted_if_the_driver_cannot_find_the_favicon (line 96) | public function fallback_is_attempted_if_the_driver_cannot_find_the_fa...
    method exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true (line 114) | public function exception_is_thrown_if_the_driver_cannot_find_the_favi...
    method default_value_can_be_returned_using_fetchOr_method (line 137) | public function default_value_can_be_returned_using_fetchOr_method(): ...
    method default_value_can_be_returned_using_fetchOr_method_with_a_closure (line 152) | public function default_value_can_be_returned_using_fetchOr_method_wit...
    method exception_can_be_thrown_after_attempting_a_fallback (line 168) | public function exception_can_be_thrown_after_attempting_a_fallback():...
    method exception_is_thrown_if_the_url_is_invalid (line 195) | public function exception_is_thrown_if_the_url_is_invalid(): void

FILE: tests/Feature/Drivers/HttpDriverTest.php
  class HttpDriverTest (line 21) | class HttpDriverTest extends TestCase
    method favicon_can_be_fetched_using_link_element_in_html (line 30) | public function favicon_can_be_fetched_using_link_element_in_html(
    method favicon_can_be_fetched_if_the_url_has_a_path_and_thelink_element_contains_a_relative_url (line 51) | public function favicon_can_be_fetched_if_the_url_has_a_path_and_theli...
    method favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html (line 65) | public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_b...
    method favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_and_a_relative_url_is_passed (line 85) | public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_b...
    method favicon_can_be_fetched_from_driver (line 110) | public function favicon_can_be_fetched_from_driver(string $protocol): ...
    method favicon_can_be_fetched_from_url_with_port (line 124) | public function favicon_can_be_fetched_from_url_with_port(): void
    method favicon_can_be_fetched_from_url_with_query_parameters (line 137) | public function favicon_can_be_fetched_from_url_with_query_parameters(...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists (line 150) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists_in_the_old_string_format (line 174) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 194) | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but...
    method null_is_returned_if_the_driver_cannot_find_the_favicon (line 213) | public function null_is_returned_if_the_driver_cannot_find_the_favicon...
    method fallback_is_attempted_if_the_driver_cannot_find_the_favicon (line 226) | public function fallback_is_attempted_if_the_driver_cannot_find_the_fa...
    method exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true (line 244) | public function exception_is_thrown_if_the_driver_cannot_find_the_favi...
    method default_value_can_be_returned_using_fetchOr_method (line 267) | public function default_value_can_be_returned_using_fetchOr_method(): ...
    method default_value_can_be_returned_using_fetchOr_method_with_a_closure (line 282) | public function default_value_can_be_returned_using_fetchOr_method_wit...
    method exception_can_be_thrown_after_attempting_a_fallback (line 298) | public function exception_can_be_thrown_after_attempting_a_fallback():...
    method exception_is_thrown_if_the_url_is_invalid (line 325) | public function exception_is_thrown_if_the_url_is_invalid(): void
    method all_icons_for_a_url_can_be_fetched (line 348) | public function all_icons_for_a_url_can_be_fetched(string $html, $expe...
    method favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_when_trying_to_get_all_icons (line 367) | public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_b...
    method empty_favicon_collection_is_returned_if_the_url_cannot_be_reached (line 388) | public function empty_favicon_collection_is_returned_if_the_url_cannot...
    method empty_favicon_collection_is_returned_if_no_icons_can_be_found_for_a_url (line 402) | public function empty_favicon_collection_is_returned_if_no_icons_can_b...
    method error_is_thrown_if_trying_to_find_all_the_favicons_for_an_invalid_url (line 422) | public function error_is_thrown_if_trying_to_find_all_the_favicons_for...
    method error_is_thrown_if_no_icons_can_be_found_for_a_url_and_the_throw_on_not_found_flag_is_true (line 441) | public function error_is_thrown_if_no_icons_can_be_found_for_a_url_and...
    method all_favicon_for_a_url_can_be_fetched_from_the_cache_if_it_already_exists (line 469) | public function all_favicon_for_a_url_can_be_fetched_from_the_cache_if...
    method all_favicons_for_a_url_are_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 515) | public function all_favicons_for_a_url_are_not_fetched_from_the_cache_...
    method favicons_can_be_returned_using_the_fetchAllOr_method (line 552) | public function favicons_can_be_returned_using_the_fetchAllOr_method()...
    method default_value_can_be_returned_using_fetchAllOr_method (line 569) | public function default_value_can_be_returned_using_fetchAllOr_method(...
    method default_value_can_be_returned_using_fetchAllOr_method_with_a_closure (line 584) | public function default_value_can_be_returned_using_fetchAllOr_method_...
    method can_set_the_user_agent_when_fetching (line 600) | public function can_set_the_user_agent_when_fetching()
    method null_is_returned_if_using_fetch_and_the_link_has_no_href (line 624) | public function null_is_returned_if_using_fetch_and_the_link_has_no_hr...
    method null_is_returned_if_using_fetchAll_and_the_link_has_no_href (line 649) | public function null_is_returned_if_using_fetchAll_and_the_link_has_no...
    method allFaviconLinksInHtmlProvider (line 673) | public static function allFaviconLinksInHtmlProvider(): array
    method faviconLinksInHtmlProvider (line 777) | public static function faviconLinksInHtmlProvider(): array
    method htmlOptionOne (line 796) | private static function htmlOptionOne(): string
    method htmlOptionTwo (line 805) | private static function htmlOptionTwo(): string
    method htmlOptionThree (line 814) | private static function htmlOptionThree(): string
    method htmlOptionFour (line 823) | private static function htmlOptionFour(): string
    method htmlOptionFive (line 832) | private static function htmlOptionFive(): string
    method htmlOptionSix (line 841) | private static function htmlOptionSix(): string
    method htmlOptionSeven (line 848) | private static function htmlOptionSeven(): string
    method htmlOptionEight (line 873) | private static function htmlOptionEight(): string
    method htmlOptionNine (line 880) | private static function htmlOptionNine(): string
    method htmlOptionTen (line 905) | private static function htmlOptionTen(): string
    method htmlOptionEleven (line 919) | private static function htmlOptionEleven(): string
    method htmlOptionTwelve (line 946) | private static function htmlOptionTwelve(): string
    method htmlOptionThirteen (line 973) | private static function htmlOptionThirteen(): string

FILE: tests/Feature/Drivers/UnavatarDriverTest.php
  class UnavatarDriverTest (line 19) | class UnavatarDriverTest extends TestCase
    method favicon_can_be_fetched_from_driver (line 29) | public function favicon_can_be_fetched_from_driver(string $protocol): ...
    method favicon_can_be_fetched_from_the_cache_if_it_already_exists (line 42) | public function favicon_can_be_fetched_from_the_cache_if_it_already_ex...
    method favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false (line 64) | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but...
    method null_is_returned_if_the_driver_cannot_find_the_favicon (line 83) | public function null_is_returned_if_the_driver_cannot_find_the_favicon...
    method fallback_is_attempted_if_the_driver_cannot_find_the_favicon (line 96) | public function fallback_is_attempted_if_the_driver_cannot_find_the_fa...
    method exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true (line 114) | public function exception_is_thrown_if_the_driver_cannot_find_the_favi...
    method default_value_can_be_returned_using_fetchOr_method (line 137) | public function default_value_can_be_returned_using_fetchOr_method(): ...
    method default_value_can_be_returned_using_fetchOr_method_with_a_closure (line 152) | public function default_value_can_be_returned_using_fetchOr_method_wit...
    method exception_can_be_thrown_after_attempting_a_fallback (line 168) | public function exception_can_be_thrown_after_attempting_a_fallback():...
    method exception_is_thrown_if_the_url_is_invalid (line 195) | public function exception_is_thrown_if_the_url_is_invalid(): void

FILE: tests/Feature/FaviconTest.php
  class FaviconTest (line 19) | class FaviconTest extends TestCase
    method favicon_url_can_be_returned (line 24) | public function favicon_url_can_be_returned(): void
    method favicon_contents_can_be_returned (line 35) | public function favicon_contents_can_be_returned(): void
    method url_can_be_returned (line 51) | public function url_can_be_returned(): void
    method retrieved_from_cache_value_can_be_returned_if_the_favicon_was_retrieved_from_the_cache (line 62) | public function retrieved_from_cache_value_can_be_returned_if_the_favi...
    method retrieved_from_cache_value_can_be_returned_if_the_favicon_was_not_retrieved_from_the_cache (line 74) | public function retrieved_from_cache_value_can_be_returned_if_the_favi...
    method favicon_can_be_cached_if_it_is_not_already_cached (line 85) | public function favicon_can_be_cached_if_it_is_not_already_cached(): void
    method favicon_cannot_be_cached_if_it_is_already_cached (line 110) | public function favicon_cannot_be_cached_if_it_is_already_cached(): void
    method favicon_can_be_cached_if_it_is_already_cached_and_the_force_flag_is_passed (line 126) | public function favicon_can_be_cached_if_it_is_already_cached_and_the_...
    method favicon_contents_be_stored (line 151) | public function favicon_contents_be_stored(): void
    method favicon_contents_be_stored_if_the_favicon_url_does_not_have_an_image_extension (line 171) | public function favicon_contents_be_stored_if_the_favicon_url_does_not...
    method favicon_contents_can_be_stored_with_a_custom_file_name (line 193) | public function favicon_contents_can_be_stored_with_a_custom_file_name...
    method icon_type_defaults_to_unknown_if_not_explicitly_set (line 210) | public function icon_type_defaults_to_unknown_if_not_explicitly_set():...
    method icon_type_can_be_set_and_returned (line 225) | public function icon_type_can_be_set_and_returned(string $expectedIcon...
    method icon_size_can_be_set_and_returned (line 242) | public function icon_size_can_be_set_and_returned(?int $expectedIconSi...
    method exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_type (line 255) | public function exception_is_thrown_when_trying_to_create_a_favicon_wi...
    method exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_size (line 267) | public function exception_is_thrown_when_trying_to_create_a_favicon_wi...
    method iconTypeProvider (line 278) | public static function iconTypeProvider(): array
    method iconSizeProvider (line 288) | public static function iconSizeProvider(): array

FILE: tests/Feature/FetcherManagerTest.php
  class FetcherManagerTest (line 19) | class FetcherManagerTest extends TestCase
    method default_driver_can_be_returned (line 24) | public function default_driver_can_be_returned(): void
    method http_driver_can_be_returned (line 32) | public function http_driver_can_be_returned(): void
    method google_shared_stuff_driver_can_be_returned (line 38) | public function google_shared_stuff_driver_can_be_returned(): void
    method favicon_kit_driver_can_be_returned (line 44) | public function favicon_kit_driver_can_be_returned(): void
    method favicon_grabber_driver_can_be_returned (line 50) | public function favicon_grabber_driver_can_be_returned(): void
    method duck_duck_go_driver_can_be_returned (line 56) | public function duck_duck_go_driver_can_be_returned(): void
    method custom_driver_can_be_returned (line 62) | public function custom_driver_can_be_returned(): void
    method exception_is_thrown_if_the_driver_is_invalid (line 69) | public function exception_is_thrown_if_the_driver_is_invalid(): void
    method method_calls_to_the_manager_are_forwarded_to_the_driver (line 78) | public function method_calls_to_the_manager_are_forwarded_to_the_drive...
    method method_calls_to_the_manager_are_forwarded_to_the_driver_using_the_facade (line 97) | public function method_calls_to_the_manager_are_forwarded_to_the_drive...
    method driver_can_be_returned_using_the_facade (line 116) | public function driver_can_be_returned_using_the_facade(): void

FILE: tests/Feature/TestCase.php
  class TestCase (line 10) | abstract class TestCase extends OrchestraTestCase
    method getPackageProviders (line 18) | protected function getPackageProviders($app)

FILE: tests/Feature/_data/CustomDriver.php
  class CustomDriver (line 11) | class CustomDriver implements Fetcher
    method fetch (line 13) | public function fetch(string $url): ?Favicon
    method fetchOr (line 22) | public function fetchOr(string $url, mixed $default): mixed
    method fetchAll (line 27) | public function fetchAll(string $url): FaviconCollection
    method fetchAllOr (line 32) | public function fetchAllOr(string $url, mixed $default): mixed

FILE: tests/Feature/_data/NullDriver.php
  class NullDriver (line 11) | class NullDriver implements Fetcher
    method fetch (line 15) | public function fetch(string $url): ?Favicon
    method fetchOr (line 22) | public function fetchOr(string $url, mixed $default): mixed
    method fetchAll (line 27) | public function fetchAll(string $url): FaviconCollection
    method fetchAllOr (line 32) | public function fetchAllOr(string $url, mixed $default): mixed
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (202K chars).
[
  {
    "path": ".gitattributes",
    "chars": 388,
    "preview": "# Ignore all test and documentation with \"export-ignore\".\n/.github           export-ignore\n/docs              export-ign"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 21,
    "preview": "github: ash-jc-allen\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 130,
    "preview": "version: 2\nupdates:\n- package-ecosystem: composer\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-"
  },
  {
    "path": ".github/workflows/ci-phpstan.yml",
    "chars": 1578,
    "preview": "name: run-phpstan\n\non:\n  pull_request:\n\njobs:\n  run-tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: fal"
  },
  {
    "path": ".github/workflows/ci-tests.yml",
    "chars": 1573,
    "preview": "name: run-tests\n\non:\n  pull_request:\n\njobs:\n  run-tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false"
  },
  {
    "path": ".gitignore",
    "chars": 50,
    "preview": ".idea/\nvendor/\ncomposer.lock\n.phpunit.result.cache"
  },
  {
    "path": ".styleci.yml",
    "chars": 15,
    "preview": "preset: laravel"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 6439,
    "preview": "# Changelog\n\n**v3.11.0 (released 2026-03-13):**\n\n- Added support for Laravel 13. ((#96)[https://github.com/ash-jc-allen/"
  },
  {
    "path": "LICENSE.md",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2022 Ashley Allen\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 23392,
    "preview": "<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:"
  },
  {
    "path": "UPGRADE.md",
    "chars": 2749,
    "preview": "# 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]("
  },
  {
    "path": "composer.json",
    "chars": 1416,
    "preview": "{\n  \"name\": \"ashallendesign/favicon-fetcher\",\n  \"description\": \"A Laravel package for fetching website's favicons.\",\n  \""
  },
  {
    "path": "config/favicon-fetcher.php",
    "chars": 2348,
    "preview": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Dr"
  },
  {
    "path": "phpstan.neon",
    "chars": 410,
    "preview": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n\nparameters:\n\n    paths:\n        - src\n\n    level: 6\n\n    igno"
  },
  {
    "path": "phpunit.xml",
    "chars": 728,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"vendor/autoload.php\"\n         backupGlobals=\"false\"\n         "
  },
  {
    "path": "src/Collections/FaviconCollection.php",
    "chars": 2143,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Collections;\n\nuse AshAllenDesign\\FaviconFetcher"
  },
  {
    "path": "src/Concerns/BuildsCacheKeys.php",
    "chars": 702,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\ntrait BuildsCacheKeys\n{\n    /**\n    "
  },
  {
    "path": "src/Concerns/HasDefaultFunctionality.php",
    "chars": 6578,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\nuse AshAllenDesign\\FaviconFetcher\\Co"
  },
  {
    "path": "src/Concerns/MakesHttpRequests.php",
    "chars": 1166,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\nuse AshAllenDesign\\FaviconFetcher\\Ex"
  },
  {
    "path": "src/Concerns/ValidatesUrls.php",
    "chars": 365,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Concerns;\n\ntrait ValidatesUrls\n{\n    /**\n     *"
  },
  {
    "path": "src/Contracts/Fetcher.php",
    "chars": 1608,
    "preview": "<?php\n\nnamespace AshAllenDesign\\FaviconFetcher\\Contracts;\n\nuse AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollecti"
  },
  {
    "path": "src/Drivers/DuckDuckGoDriver.php",
    "chars": 2238,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Drivers/FaviconGrabberDriver.php",
    "chars": 2354,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Drivers/FaviconKitDriver.php",
    "chars": 2121,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Drivers/GoogleSharedStuffDriver.php",
    "chars": 2159,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Drivers/HttpDriver.php",
    "chars": 8315,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Drivers/UnavatarDriver.php",
    "chars": 2128,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Drivers;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Exceptions/ConnectionException.php",
    "chars": 129,
    "preview": "<?php\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass ConnectionException extends FaviconFetcherException\n{\n"
  },
  {
    "path": "src/Exceptions/FaviconFetcherException.php",
    "chars": 161,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nuse Exception;\n\nclass FaviconFetch"
  },
  {
    "path": "src/Exceptions/FaviconNotFoundException.php",
    "chars": 160,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass FaviconNotFoundException ext"
  },
  {
    "path": "src/Exceptions/FeatureNotSupportedException.php",
    "chars": 164,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass FeatureNotSupportedException"
  },
  {
    "path": "src/Exceptions/InvalidIconSizeException.php",
    "chars": 160,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass InvalidIconSizeException ext"
  },
  {
    "path": "src/Exceptions/InvalidIconTypeException.php",
    "chars": 160,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass InvalidIconTypeException ext"
  },
  {
    "path": "src/Exceptions/InvalidUrlException.php",
    "chars": 155,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Exceptions;\n\nclass InvalidUrlException extends "
  },
  {
    "path": "src/Facades/Favicon.php",
    "chars": 1146,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Facades;\n\nuse AshAllenDesign\\FaviconFetcher\\Col"
  },
  {
    "path": "src/Favicon.php",
    "chars": 6672,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher;\n\nuse AshAllenDesign\\FaviconFetcher\\Concerns\\Bu"
  },
  {
    "path": "src/FaviconFetcherProvider.php",
    "chars": 766,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher;\n\nuse Illuminate\\Support\\ServiceProvider;\n\nclas"
  },
  {
    "path": "src/FetcherManager.php",
    "chars": 1747,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher;\n\nuse AshAllenDesign\\FaviconFetcher\\Contracts\\F"
  },
  {
    "path": "tests/Feature/Collections/FaviconCollectionTest.php",
    "chars": 7774,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Collections;\n\nuse AshAllenDesign\\"
  },
  {
    "path": "tests/Feature/Concerns/MakesHttpRequests/HttpClientTest.php",
    "chars": 1339,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Concerns\\MakesHttpRequests;\n\nuse "
  },
  {
    "path": "tests/Feature/Concerns/MakesHttpRequests/WithRequestExceptionHandlingTest.php",
    "chars": 763,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Concerns\\MakesHttpRequests;\n\nuse "
  },
  {
    "path": "tests/Feature/Drivers/DuckDuckGoDriverTest.php",
    "chars": 6943,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\Favi"
  },
  {
    "path": "tests/Feature/Drivers/FaviconGrabberDriverTest.php",
    "chars": 9370,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\Favi"
  },
  {
    "path": "tests/Feature/Drivers/FaviconKitDriverTest.php",
    "chars": 6843,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\Favi"
  },
  {
    "path": "tests/Feature/Drivers/GoogleSharedStuffDriverTest.php",
    "chars": 7173,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\Favi"
  },
  {
    "path": "tests/Feature/Drivers/HttpDriverTest.php",
    "chars": 44158,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\Favi"
  },
  {
    "path": "tests/Feature/Drivers/UnavatarDriverTest.php",
    "chars": 6869,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\Drivers;\n\nuse AshAllenDesign\\Favi"
  },
  {
    "path": "tests/Feature/FaviconTest.php",
    "chars": 8914,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature;\n\nuse AshAllenDesign\\FaviconFetch"
  },
  {
    "path": "tests/Feature/FetcherManagerTest.php",
    "chars": 3856,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature;\n\nuse AshAllenDesign\\FaviconFetch"
  },
  {
    "path": "tests/Feature/TestCase.php",
    "chars": 468,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature;\n\nuse AshAllenDesign\\FaviconFetch"
  },
  {
    "path": "tests/Feature/_data/CustomDriver.php",
    "chars": 884,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data;\n\nuse AshAllenDesign\\Favico"
  },
  {
    "path": "tests/Feature/_data/NullDriver.php",
    "chars": 818,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace AshAllenDesign\\FaviconFetcher\\Tests\\Feature\\_data;\n\nuse AshAllenDesign\\Favico"
  }
]

About this extraction

This page contains the full source code of the ash-jc-allen/favicon-fetcher GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (187.3 KB), approximately 47.0k tokens, and a symbol index with 250 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!