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 `` 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 ================================================

Favicon Fetcher

Latest Version on Packagist Total Downloads PHP from Packagist GitHub license

## 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 ================================================ '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 ================================================ tests src/ ================================================ FILE: src/Collections/FaviconCollection.php ================================================ */ class FaviconCollection extends Collection { use HasDefaultFunctionality; /** * Whether the favicons in this collection were all retrieved from the cache. */ protected bool $retrievedFromCache = false; /** * @param array> $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 ================================================ buildCacheKey($url).'.collection'; } } ================================================ FILE: src/Concerns/HasDefaultFunctionality.php ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 */ public function toCache(): array { return [ 'favicon_url' => $this->getFaviconUrl(), 'icon_size' => $this->getIconSize(), 'icon_type' => $this->getIconType(), ]; } } ================================================ FILE: src/FaviconFetcherProvider.php ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ expectException(ConnectionException::class); $this->withRequestExceptionHandling(function () { throw new ClientConnectionException('Test exception'); }); } } ================================================ FILE: tests/Feature/Drivers/DuckDuckGoDriverTest.php ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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 ================================================ 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; 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; 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(''), 'http://example.com' => Http::response(''), '*' => 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(''), '*' => 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(''), '*' => 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(''), '*' => 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; 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; 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; 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(''), '*' => 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' Dummy title 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' Dummy title 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; } private static function htmlOptionTwo(): string { return <<<'HTML' HTML; } private static function htmlOptionThree(): string { return <<<'HTML' HTML; } private static function htmlOptionFour(): string { return <<<'HTML' HTML; } private static function htmlOptionFive(): string { return <<<'HTML' HTML; } private static function htmlOptionSix(): string { return <<<'HTML' Title here HTML; } private static function htmlOptionEight(): string { return <<<'HTML' Title here HTML; } private static function htmlOptionTen(): string { return <<<'HTML' Test Title HTML; } private static function htmlOptionEleven(): string { return <<<'HTML' Dummy title HTML; } private static function htmlOptionTwelve(): string { return <<<'HTML' Dummy title HTML; } private static function htmlOptionThirteen(): string { return <<<'HTML' Dummy title HTML; } } ================================================ FILE: tests/Feature/Drivers/UnavatarDriverTest.php ================================================ 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 ================================================ 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 ================================================ '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 ================================================