├── .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
├── docs
└── logo.png
├── phpstan.neon
├── phpunit.xml
├── src
├── Collections
│ └── FaviconCollection.php
├── Concerns
│ ├── BuildsCacheKeys.php
│ ├── HasDefaultFunctionality.php
│ ├── MakesHttpRequests.php
│ └── ValidatesUrls.php
├── Contracts
│ └── Fetcher.php
├── Drivers
│ ├── 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
├── FaviconGrabberDriverTest.php
├── FaviconKitDriverTest.php
├── GoogleSharedStuffDriverTest.php
├── HttpDriverTest.php
└── UnavatarDriverTest.php
├── FaviconTest.php
├── FetcherManagerTest.php
├── TestCase.php
└── _data
├── CustomDriver.php
└── NullDriver.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ash-jc-allen
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci-phpstan.yml:
--------------------------------------------------------------------------------
1 | name: run-phpstan
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | run-tests:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | php: [8.1, 8.2, 8.3, 8.4]
13 | laravel: [9.*, 10.*, 11.*, 12.*]
14 | include:
15 | - laravel: 12.*
16 | testbench: 10.*
17 | - laravel: 11.*
18 | testbench: 9.*
19 | - laravel: 10.*
20 | testbench: 8.*
21 | - laravel: 9.*
22 | testbench: 7.*
23 | exclude:
24 | - php: 8.1
25 | laravel: 11.*
26 | - php: 8.1
27 | laravel: 12.*
28 |
29 | name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}
30 |
31 | steps:
32 | - name: Update apt
33 | run: sudo apt-get update --fix-missing
34 |
35 | - name: Checkout code
36 | uses: actions/checkout@v2
37 |
38 | - name: Setup PHP
39 | uses: shivammathur/setup-php@v2
40 | with:
41 | php-version: ${{ matrix.php }}
42 | coverage: none
43 |
44 | - name: Setup Problem Matches
45 | run: |
46 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
47 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
48 |
49 | - name: Install dependencies
50 | run: |
51 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
52 | composer update --prefer-dist --no-interaction --no-suggest
53 | - name: Run Larastan
54 | run: composer larastan
55 |
--------------------------------------------------------------------------------
/.github/workflows/ci-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | run-tests:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | php: [8.1, 8.2, 8.3, 8.4]
13 | laravel: [9.*, 10.*, 11.*, 12.*]
14 | include:
15 | - laravel: 12.*
16 | testbench: 10.*
17 | - laravel: 11.*
18 | testbench: 9.*
19 | - laravel: 10.*
20 | testbench: 8.*
21 | - laravel: 9.*
22 | testbench: 7.*
23 | exclude:
24 | - php: 8.1
25 | laravel: 11.*
26 | - php: 8.1
27 | laravel: 12.*
28 |
29 | name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}
30 |
31 | steps:
32 | - name: Update apt
33 | run: sudo apt-get update --fix-missing
34 |
35 | - name: Checkout code
36 | uses: actions/checkout@v2
37 |
38 | - name: Setup PHP
39 | uses: shivammathur/setup-php@v2
40 | with:
41 | php-version: ${{ matrix.php }}
42 | coverage: none
43 |
44 | - name: Setup Problem Matches
45 | run: |
46 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
47 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
48 |
49 | - name: Install dependencies
50 | run: |
51 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
52 | composer update --prefer-dist --no-interaction --no-suggest
53 | - name: Execute tests
54 | run: composer test
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | vendor/
3 | composer.lock
4 | .phpunit.result.cache
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: laravel
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | **v3.8.0 (released 2025-02-24):**
4 |
5 | - Added support for Laravel 12. ([#82](https://github.com/ash-jc-allen/favicon-fetcher/pull/82))
6 | - Added support for PHPUnit 11. ([#83](https://github.com/ash-jc-allen/favicon-fetcher/pull/83))
7 | - Migrated from `nunomaduro/larastan` to `larastan/larastan`. ([#84](https://github.com/ash-jc-allen/favicon-fetcher/pull/84))
8 | - Added support for Larastan 3. ([#84](https://github.com/ash-jc-allen/favicon-fetcher/pull/84))
9 |
10 | **v3.7.0 (released 2024-11-30):**
11 |
12 | - Added explicit nullable types to support PHP 8.4. ([#80](https://github.com/ash-jc-allen/favicon-fetcher/pull/80))
13 |
14 | **v3.6.0 (released 2024-07-08):**
15 |
16 | - Added support for `symfony/dom-crawler` v7.0. ([#79](https://github.com/ash-jc-allen/favicon-fetcher/pull/79))
17 |
18 | **v3.5.0 (released 2024-06-14):**
19 |
20 | - Added a new `verify_tls` config option to disable TLS certificate verification. ([#78](https://github.com/ash-jc-allen/favicon-fetcher/pull/78))
21 |
22 | **v3.4.1 (released 2024-04-30):**
23 |
24 | - 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))
25 |
26 | **v3.4.0 (released 2024-03-19):**
27 |
28 | - Added support for `nesbot/carbon 3.0`. ([#76](https://github.com/ash-jc-allen/favicon-fetcher/pull/76))
29 |
30 | **v3.3.0 (released 2024-03-12):**
31 |
32 | - Added support for Laravel 11. ([#75](https://github.com/ash-jc-allen/favicon-fetcher/pull/75))
33 |
34 | **v3.2.0 (released 2024-01-29):**
35 |
36 | - Added a `largestByFileSize` method to the `FaviconCollection`. [#73](https://github.com/ash-jc-allen/favicon-fetcher/pull/73)
37 |
38 | **v3.1.0 (released 2023-11-07):**
39 |
40 | - Added `user_agent` config field to configure HTTP `User-Agent` request header. ([#70](https://github.com/ash-jc-allen/favicon-fetcher/pull/70))
41 | - Run CI tests using PHP 8.3 ([#69](https://github.com/ash-jc-allen/favicon-fetcher/pull/69))
42 |
43 | **v3.0.0 (released 2023-09-04):**
44 |
45 | - Added `connect_timeout` and `timeout` config fields. ([#67](https://github.com/ash-jc-allen/favicon-fetcher/pull/67))
46 | - Use `symfony/dom-crawler` in the `HttpDriver` to parse the HTML. ([#56](https://github.com/ash-jc-allen/favicon-fetcher/pull/56))
47 | - Updated all files to use strict types for improved type safety. ([#62](https://github.com/ash-jc-allen/favicon-fetcher/pull/62))
48 | - Throw package-specific exceptions instead of vendor exceptions. ([#67](https://github.com/ash-jc-allen/favicon-fetcher/pull/67))
49 | - 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))
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))
51 | - 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))
52 | - Dropped support for PHP 8.0. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))
53 | - Dropped support for Laravel 8. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))
54 | - Dropped support for PHPUnit 8.* and Larastan 1.*. ([#59](https://github.com/ash-jc-allen/favicon-fetcher/pull/59))
55 |
56 | **v2.0.0 (released 2023-03-23):**
57 | - Added driver for the [Favicon Grabber API](https://favicongrabber.com/). ([#24](https://github.com/ash-jc-allen/favicon-fetcher/pull/24))
58 | - 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))
59 | - Added `fetchAll` method to the `AshAllenDesign\FaviconFetcher\Contracts\Fetcher` interface. ([#29](https://github.com/ash-jc-allen/favicon-fetcher/pull/29))
60 | - 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))
61 | - 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))
62 | - Changed the values that are used when caching a favicon. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))
63 | - Removed the `makeFromCache` method from the `Favicon` class. ([#31](https://github.com/ash-jc-allen/favicon-fetcher/pull/31))
64 |
65 | **v1.3.0 (released 2023-01-12):**
66 | - Added support for Laravel 10. (([#22](https://github.com/ash-jc-allen/favicon-fetcher/pull/22)))
67 |
68 | **v1.2.1 (released 2022-11-08):**
69 | - 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))
70 |
71 | **v1.2.0 (released 2022-10-17):**
72 | - Added support for PHP 8.2. ([#21](https://github.com/ash-jc-allen/favicon-fetcher/pull/21))
73 |
74 | **v1.1.3 (released 2022-09-03):**
75 | - Removed an incorrect mime type from the file extension detection. ([#19](https://github.com/ash-jc-allen/favicon-fetcher/pull/19))
76 |
77 | **v1.1.2 (released 2022-07-23):**
78 | - 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))
79 |
80 | **v1.1.1 (released 2022-05-10):**
81 | - 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))
82 |
83 | **v1.1.0 (released 2022-04-27):**
84 | - Added driver for [Unavatar](https://unavatar). ([#8](https://github.com/ash-jc-allen/favicon-fetcher/pull/8))
85 |
86 | **v1.0.0 (released 2022-04-26):**
87 | - Initial release.
88 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Ashley Allen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## Table of Contents
13 |
14 | - [Overview](#overview)
15 | - [Installation](#installation)
16 | * [Requirements](#requirements)
17 | * [Install the Package](#install-the-package)
18 | * [Publish the Config](#publish-the-config)
19 | - [Usage](#usage)
20 | * [Fetching Favicons](#fetching-favicons)
21 | + [Using the `fetch` Method](#using-the-fetch-method)
22 | + [Using the `fetchOr` Method](#using-the-fetchor-method)
23 | + [Using the `fetchAll` Method](#using-the-fetchall-method)
24 | + [Using the `fetchAllOr` Method](#using-the-fetchallor-method)
25 | * [Exceptions](#exceptions)
26 | * [Drivers](#drivers)
27 | * [Available Drivers](#available-drivers)
28 | + [How to Choose a Driver](#how-to-choose-a-driver)
29 | * [Choosing a Driver](#choosing-a-driver)
30 | + [Fallback Drivers](#fallback-drivers)
31 | + [Adding Your Own Driver](#adding-your-own-driver)
32 | * [HTTP Timeouts](#http-timeouts)
33 | * [TLS Verification](#tls-verification)
34 | * [HTTP User Agent](#http-user-agent)
35 | * [Storing Favicons](#storing-favicons)
36 | + [Using `store`](#using-store)
37 | + [Using `storeAs`](#using-storeas)
38 | * [Caching Favicons](#caching-favicons)
39 | * [Favicon Types](#favicon-types)
40 | * [Favicon Sizes](#favicon-sizes)
41 | - [Testing](#testing)
42 | - [Security](#security)
43 | - [Contribution](#contribution)
44 | - [Changelog](#changelog)
45 | - [Upgrading](#upgrading)
46 | - [Credits](#credits)
47 | - [License](#license)
48 |
49 | ## Overview
50 |
51 | A Laravel package that can be used for fetching favicons from websites.
52 |
53 | ## Installation
54 |
55 | ### Requirements
56 |
57 | The package has been developed and tested to work with the following minimum requirements:
58 |
59 | - PHP 8.0
60 | - Laravel 8.0
61 |
62 | ### Install the Package
63 |
64 | You can install the package via Composer:
65 |
66 | ```bash
67 | composer require ashallendesign/favicon-fetcher
68 | ```
69 |
70 | ### Publish the Config
71 | You can then publish the package's config file by using the following command:
72 |
73 | ```bash
74 | php artisan vendor:publish --provider="AshAllenDesign\FaviconFetcher\FaviconFetcherProvider"
75 | ```
76 |
77 | ## Usage
78 |
79 | ### Fetching Favicons
80 |
81 | Now that you have the package installed, you can start fetching the favicons from different websites.
82 |
83 | #### Using the `fetch` Method
84 |
85 | To fetch a favicon from a website, you can use the `fetch` method which will return an instance of `AshAllenDesign\FaviconFetcher\Favicon`:
86 |
87 | ```php
88 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
89 |
90 | $favicon = Favicon::fetch('https://ashallendesign.co.uk');
91 | ```
92 |
93 | #### Using the `fetchOr` Method
94 |
95 | If you'd like to provide a default value to be used if a favicon cannot be found, you can use the `fetchOr` method.
96 |
97 | 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:
98 |
99 | ```php
100 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
101 |
102 | $favicon = Favicon::fetchOr('https://ashallendesign.co.uk', 'https://example.com/favicon.ico');
103 | ```
104 |
105 | 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:
106 |
107 | ```php
108 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
109 |
110 | $favicon = Favicon::fetchOr('https://ashallendesign.co.uk', function ($url) {
111 | // Run extra logic here...
112 |
113 | return 'https://example.com/favicon.ico';
114 | });
115 | ```
116 |
117 | #### Using the `fetchAll` Method
118 |
119 | 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:
120 |
121 | ```php
122 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
123 |
124 | $favicons = Favicon::fetchAll('https://ashallendesign.co.uk');
125 | ```
126 |
127 | The `FaviconCollection` class extends the `Illuminate\Support\Collection` class, so you can use all the methods available on the `Collection` class.
128 |
129 | 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:
130 |
131 | ```php
132 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
133 |
134 | $largestFavicon = Favicon::fetchAll('https://ashallendesign.co.uk')->largest();
135 | ```
136 |
137 | 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:
138 |
139 | ```php
140 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
141 |
142 | $largestFavicon = Favicon::fetchAll('https://ashallendesign.co.uk')->largestByFileSize();
143 | ```
144 |
145 | 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.
146 |
147 | #### Using the `fetchAllOr` Method
148 |
149 | 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.
150 |
151 | 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:
152 |
153 | ```php
154 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
155 |
156 | $favicon = Favicon::fetchAllOr('https://ashallendesign.co.uk', 'https://example.com/favicon.ico');
157 | ```
158 |
159 | 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:
160 |
161 | ```php
162 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
163 |
164 | $favicon = Favicon::fetchAllOr('https://ashallendesign.co.uk', function ($url) {
165 | // Run extra logic here...
166 |
167 | return 'https://example.com/favicon.ico';
168 | });
169 | ```
170 |
171 | ### Exceptions
172 |
173 | 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.
174 |
175 | To enable exceptions to be thrown, your code could look something like this:
176 |
177 | ```php
178 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
179 |
180 | $favicon = Favicon::throw()->fetch('https://ashallendesign.co.uk');
181 | ```
182 |
183 | 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.
184 |
185 | ### Drivers
186 |
187 | Favicon Fetcher provides the functionality to use different drivers for retrieving favicons from websites.
188 |
189 | ### Available Drivers
190 |
191 | By default, Favicon Fetcher ships with 5 drivers out-the-box: `http`, `google-shared-stuff`, `favicon-kit`, `unavatar`, `favicon-grabber`.
192 |
193 | 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.
194 |
195 | The `google-shared-stuff` driver fetches favicons using the [Google Shared Stuff](https://google.com) API.
196 |
197 | The `favicon-kit` driver fetches favicons using the [Favicon Kit](https://faviconkit.com) API.
198 |
199 | The `unavatar` driver fetches favicons using the [Unavatar](https://unavatar.io) API.
200 |
201 | The `favicon-grabber` driver fetches favicons using the [Favicon Grabber](https://favicongrabber.com) API.
202 |
203 | #### How to Choose a Driver
204 |
205 | It's important to remember that the `google-shared-stuff`, `favicon-kit`, and `unavatar` drivers interact with third-party APIs to retrieve the favicons. So, this means that some data will be shared to external services.
206 |
207 | 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.
208 |
209 | ### Choosing a Driver
210 |
211 | 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.
212 |
213 | For example, if you wanted to change your default driver to `favicon-kit`, you could update your `favicon-fetcher` config like so:
214 |
215 | ```php
216 | return [
217 |
218 | // ...
219 |
220 | 'default' => 'favicon-kit',
221 |
222 | // ...
223 |
224 | ]
225 | ```
226 |
227 | 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:
228 |
229 | ```php
230 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
231 |
232 | $favicon = Favicon::driver('google-shared-stuff')->fetch('https://ashallendesign.co.uk');
233 | ```
234 |
235 | #### Fallback Drivers
236 |
237 | 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.
238 |
239 | 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:
240 |
241 | ```php
242 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
243 |
244 | $favicon = Favicon::withFallback('google-shared-stuff')->fetch('https://ashallendesign.co.uk');
245 | ```
246 |
247 | #### Adding Your Own Driver
248 |
249 | 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.
250 |
251 | 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:
252 |
253 | ```php
254 | use AshAllenDesign\FaviconFetcher\Contracts\Fetcher;
255 | use AshAllenDesign\FaviconFetcher\Favicon;
256 |
257 | class MyCustomDriver implements Fetcher
258 | {
259 | public function fetch(string $url): ?Favicon
260 | {
261 | // Add logic here that attempts to fetch a favicon...
262 | }
263 |
264 | public function fetchOr(string $url, mixed $default): mixed
265 | {
266 | // Add logic here that attempts to fetch a favicon or return a default...
267 | }
268 | }
269 | ```
270 |
271 | 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.
272 |
273 | You can register your custom driver like so:
274 |
275 | ```php
276 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
277 |
278 | Favicon::extend('my-custom-driver', new MyCustomDriver());
279 | ```
280 |
281 | Now that you've registered your custom driver, you'll be able to use it for fetching favicons like so:
282 |
283 | ```php
284 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
285 |
286 | $favicon = Favicon::driver('my-custom-driver')->fetch('https://ashallendesign.co.uk');
287 | ```
288 |
289 | ### HTTP Timeouts
290 |
291 | Favicon Fetcher provides the ability for you to set the connection timeout and request timeout for all the drivers.
292 |
293 | 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.
294 |
295 | 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:
296 |
297 | ```php
298 | return [
299 |
300 | // ...
301 |
302 | 'connect_timeout' => 5,
303 |
304 | 'timeout' => 10,
305 |
306 | // ...
307 |
308 | ]
309 | ```
310 |
311 | If you'd prefer that no timeout be set, you can set the values to `0`.
312 |
313 | Please note that these timeouts are applied to all HTTP requests that Favicon Fetcher makes, regardless of the driver that is being used.
314 |
315 | ### TLS Verification
316 |
317 | 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.
318 |
319 | You can disable the verification by updating the `verify_tls` field in the `favicon-fetcher.php` config file after you've published it.
320 |
321 | ```php
322 | return [
323 |
324 | // ...
325 |
326 | 'verify_tls' => false,
327 |
328 | // ...
329 |
330 | ]
331 | ```
332 |
333 | Or by updating your `.env` file:
334 |
335 | ```dotenv
336 | FAVICON_FETCHER_VERIFY_TLS=false
337 | ```
338 |
339 | ### HTTP User Agent
340 |
341 | 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.
342 |
343 | 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:
344 |
345 | ```php
346 | return [
347 |
348 | // ...
349 |
350 | 'user_agent' => 'My Custom User Agent',
351 |
352 | // ...
353 |
354 | ]
355 | ```
356 |
357 | The `User-Agent` header will be set on all HTTP requests that Favicon Fetcher makes, regardless of the driver that is being used.
358 |
359 | 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:
360 |
361 | ```dotenv
362 | FAVICON_FETCHER_USER_AGENT="My Custom User Agent"
363 | ```
364 |
365 | ### Storing Favicons
366 |
367 | 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`.
368 |
369 | #### Using `store`
370 |
371 | 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:
372 |
373 | ```php
374 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
375 |
376 | $faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->store('favicons');
377 |
378 | // $faviconPath is now equal to: "/favicons/abc-123.ico"
379 | ```
380 |
381 | 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:
382 |
383 | ```php
384 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
385 |
386 | $faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->store('favicons', 's3');
387 |
388 | // $faviconPath is now equal to: "/favicons/abc-123.ico"
389 | ```
390 |
391 | #### Using `storeAs`
392 |
393 | 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:
394 |
395 | ```php
396 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
397 |
398 | $faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->storeAs('favicons', 'ashallendesign');
399 |
400 | // $faviconPath is now equal to: "/favicons/ashallendesign.ico"
401 | ```
402 |
403 | 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:
404 |
405 | ```php
406 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
407 |
408 | $faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->storeAs('favicons', 'ashallendesign', 's3');
409 |
410 | // $faviconPath is now equal to: "/favicons/ashallendesign.ico"
411 | ```
412 |
413 | ### Caching Favicons
414 |
415 | 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.
416 |
417 | 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.
418 |
419 | 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:
420 |
421 | ```php
422 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
423 |
424 | $favicon = Favicon::fetch('https://ashallendesign.co.uk')->cache(now()->addDay());
425 | ```
426 |
427 | 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:
428 |
429 | ```php
430 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
431 |
432 | $favicon = Favicon::useCache(false)->fetch('https://ashallendesign.co.uk');
433 | ```
434 |
435 | 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:
436 |
437 | ```php
438 | return [
439 |
440 | // ...
441 |
442 | 'cache' => [
443 | 'prefix' => 'my-awesome-prefix',
444 | ]
445 |
446 | // ...
447 |
448 | ]
449 | ```
450 |
451 | 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:
452 |
453 | ```php
454 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
455 |
456 | $faviconCollection = Favicon::fetchAll('https://ashallendesign.co.uk')->cache(now()->addDay());
457 | ```
458 |
459 | ### Favicon Types
460 |
461 | 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:
462 |
463 | ```php
464 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
465 |
466 | $faviconPath = Favicon::fetch('https://ashallendesign.co.uk')->getIconType();
467 | ```
468 |
469 | 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`.
470 |
471 | 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:
472 |
473 | ```php
474 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
475 |
476 | $faviconCollection = Favicon::fetchAll('https://ashallendesign.co.uk');
477 |
478 | $faviconCollection->filter(function ($favicon) {
479 | return $favicon->getIconType() !== Favicon::TYPE_APPLE_TOUCH_ICON;
480 | });
481 | ```
482 |
483 | ### Favicon Sizes
484 |
485 | 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:
486 |
487 | ```php
488 | use AshAllenDesign\FaviconFetcher\Facades\Favicon;
489 |
490 | $faviconSize = Favicon::fetch('https://ashallendesign.co.uk')->getIconSize();
491 | ```
492 |
493 | 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.
494 |
495 | ## Testing
496 |
497 | To run the package's unit tests, run the following command:
498 |
499 | ``` bash
500 | composer test
501 | ```
502 |
503 | To run Larastan for the package, run the following command:
504 |
505 | ```bash
506 | composer larastan
507 | ```
508 |
509 | ## Security
510 |
511 | If you find any security related issues, please contact me directly at [mail@ashallendesign.co.uk](mailto:mail@ashallendesign.co.uk) to report it.
512 |
513 | ## Contribution
514 |
515 | If you wish to make any changes or improvements to the package, feel free to make a pull request.
516 |
517 | To contribute to this package, please use the following guidelines before submitting your pull request:
518 |
519 | - Write tests for any new functions that are added. If you are updating existing code, make sure that the existing tests
520 | pass and write more if needed.
521 | - Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standards.
522 | - Make all pull requests to the `master` branch.
523 |
524 | ## Changelog
525 |
526 | Check the [CHANGELOG](CHANGELOG.md) to get more information about the latest changes.
527 |
528 | ## Upgrading
529 |
530 | Check the [UPGRADE](UPGRADE.md) guide to get more information on how to update this library to newer versions.
531 |
532 | ## Credits
533 |
534 | - [Ash Allen](https://ashallendesign.co.uk)
535 | - [Jess Pickup](https://jesspickup.co.uk) (Logo)
536 | - [All Contributors](https://github.com/ash-jc-allen/short-url/graphs/contributors)
537 |
538 | ## License
539 |
540 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
541 |
542 | ## Support Me
543 |
544 | 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.
545 |
546 | Every sale makes a huge difference to me and allows me to spend more time working on open-source projects and tutorials.
547 |
548 | To say a huge thanks, you can use the code **BATTLE20** to get a 20% discount on the book.
549 |
550 | [👉 Get Your Copy!](https://battle-ready-laravel.com)
551 |
552 | [](https://battle-ready-laravel.com)
553 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # Upgrade Guide
2 |
3 | ## Contents
4 |
5 | - [Upgrading from 2.* to 3.0.0](#upgrading-from-2-to-300)
6 | - [Upgrading from 1.* to 2.0.0](#upgrading-from-1-to-200)
7 |
8 | ## Upgrading from 2.* to 3.0.0
9 |
10 | ### Exceptions
11 |
12 | 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.
13 |
14 | This will be thrown in the following situations:
15 |
16 | - If the URL has a valid structure but the site doesn't exist.
17 | - If the HTTP client exceeds the connection timeout.
18 | - If the HTTP client exceeds the request timeout.
19 |
20 | ## Upgrading from 1.* to 2.0.0
21 |
22 | ### Method Visibility Changes
23 |
24 | 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`.
25 |
26 | ### Added `fetchAll` and `fetchAllOr` Methods to `Fetcher` Interface
27 |
28 | 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.
29 |
30 | The signatures for the new methods are:
31 |
32 | ```php
33 | public function fetchAll(string $url): FaviconCollection;
34 | ```
35 |
36 | ```php
37 | public function fetchAllOr(string $url, mixed $default): mixed;
38 | ```
39 |
40 | ### Removed `makeFromCache` Method from `Favicon` Class
41 |
42 | 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.
43 |
44 | If you were making use of this method anywhere, you'll need to remove it from your code.
45 |
46 | ### Caching Changes
47 |
48 | 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.
49 |
50 | This means that instead of a string being stored in the cache, an array is now stored instead.
51 |
52 | 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.
53 |
54 | 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.
55 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ashallendesign/favicon-fetcher",
3 | "description": "A Laravel package for fetching website's favicons.",
4 | "type": "library",
5 | "homepage": "https://github.com/ash-jc-allen/favicon-fetcher",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Ash Allen",
10 | "email": "mail@ashallendesign.co.uk"
11 | }
12 | ],
13 | "keywords": [
14 | "ashallendesign",
15 | "favicon-fetcher",
16 | "favicon",
17 | "icon"
18 | ],
19 | "require": {
20 | "php": "^8.1",
21 | "nesbot/carbon": "^2.0|^3.0",
22 | "illuminate/cache": "^9.0|^10.0|^11.0|^12.0",
23 | "illuminate/filesystem": "^9.0|^10.0|^11.0|^12.0",
24 | "illuminate/http": "^9.0|^10.0|^11.0|^12.0",
25 | "guzzlehttp/guzzle": "^7.4",
26 | "symfony/dom-crawler": "^6.3 || ^7.0"
27 | },
28 | "require-dev": {
29 | "mockery/mockery": "^1.0",
30 | "orchestra/testbench": "^7.0|^8.0|^9.0",
31 | "phpunit/phpunit": "^9.0|^10.0|^11.0",
32 | "larastan/larastan": "^2.0|^3.0"
33 | },
34 | "autoload": {
35 | "psr-4": {
36 | "AshAllenDesign\\FaviconFetcher\\": "src/"
37 | }
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "AshAllenDesign\\FaviconFetcher\\Tests\\": "tests/"
42 | }
43 | },
44 | "extra": {
45 | "laravel": {
46 | "providers": [
47 | "AshAllenDesign\\FaviconFetcher\\FaviconFetcherProvider"
48 | ]
49 | }
50 | },
51 | "scripts": {
52 | "test": "vendor/bin/phpunit",
53 | "larastan": "vendor/bin/phpstan analyse"
54 | },
55 | "minimum-stability": "dev",
56 | "prefer-stable": true
57 | }
58 |
--------------------------------------------------------------------------------
/config/favicon-fetcher.php:
--------------------------------------------------------------------------------
1 | 'http',
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Caching
23 | |--------------------------------------------------------------------------
24 | |
25 | | The package provides support for caching the fetched favicon's URLs.
26 | | Here, you can specify the different options for caching, such as
27 | | cache prefix that is prepended to all the cache keys.
28 | |
29 | */
30 | 'cache' => [
31 | 'prefix' => 'favicon-fetcher',
32 | ],
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | HTTP Timeouts
37 | |--------------------------------------------------------------------------
38 | |
39 | | Set the timeouts here in seconds for the HTTP requests that are made
40 | | to fetch the favicons. If the timeout is set to 0, then no timeout
41 | | will be applied. The connect timeout is the time taken to connect
42 | | to the server, while the timeout is the time taken to get a
43 | | response from the server after the connection is made.
44 | |
45 | */
46 | 'timeout' => 0,
47 |
48 | 'connect_timeout' => 0,
49 |
50 | /*
51 | |--------------------------------------------------------------------------
52 | | Verify TLS
53 | |--------------------------------------------------------------------------
54 | |
55 | | Sets the TLS verification option when making HTTP requests, which is
56 | | enabled by default.
57 | */
58 | 'verify_tls' => env('FAVICON_FETCHER_VERIFY_TLS', true),
59 |
60 | /*
61 | |--------------------------------------------------------------------------
62 | | HTTP User Agent
63 | |--------------------------------------------------------------------------
64 | |
65 | | Set the user agent used by the HTTP client when fetching the favicons.
66 | |
67 | */
68 | 'user_agent' => env('FAVICON_FETCHER_USER_AGENT'),
69 | ];
70 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ash-jc-allen/favicon-fetcher/e9e79ec1286066d216ecffdad360cd28ea2a666a/docs/logo.png
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/larastan/larastan/extension.neon
3 |
4 | parameters:
5 |
6 | paths:
7 | - src
8 |
9 | level: 6
10 |
11 | ignoreErrors:
12 | - "#^Unsafe usage of new static#"
13 | - '#^Call to an undefined method AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection::fetch\(\)#'
14 | - '#^Call to an undefined method AshAllenDesign\\FaviconFetcher\\Collections\\FaviconCollection::fetchAll\(\)#'
15 |
16 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/Collections/FaviconCollection.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class FaviconCollection extends Collection
17 | {
18 | use HasDefaultFunctionality;
19 |
20 | /**
21 | * Whether the favicons in this collection were all retrieved from the cache.
22 | */
23 | protected bool $retrievedFromCache = false;
24 |
25 | /**
26 | * @param array> $items
27 | * @return static
28 | */
29 | public static function makeFromCache(array $items = []): static
30 | {
31 | $collection = new static($items);
32 |
33 | $collection->retrievedFromCache = true;
34 |
35 | return $collection;
36 | }
37 |
38 | /**
39 | * Cache the collection of favicons. We only cache the collection if it contains
40 | * items and if it was not retrieved from the cache. If the collection was
41 | * retrieved from the cache, then the "force" flag has to be set to
42 | * true in order to cache it.
43 | */
44 | public function cache(CarbonInterface $ttl, bool $force = false): self
45 | {
46 | $shouldCache = $this->isNotEmpty()
47 | && ($force || ! $this->retrievedFromCache);
48 |
49 | if ($shouldCache) {
50 | $cacheKey = $this->buildCacheKeyForCollection($this->first()->getUrl());
51 |
52 | $cacheData = $this->map(fn (Favicon $favicon): array => $favicon->toCache())->all();
53 |
54 | Cache::put($cacheKey, $cacheData, $ttl);
55 | }
56 |
57 | return $this;
58 | }
59 |
60 | /**
61 | * Get the favicon with the largest icon size. Any icons with an unknown size (null)
62 | * will be treated as having a size of 0.
63 | */
64 | public function largest(): ?Favicon
65 | {
66 | return $this->sortByDesc(
67 | fn (Favicon $favicon): ?int => $favicon->getIconSize()
68 | )->first();
69 | }
70 |
71 | public function largestByFileSize(): ?Favicon
72 | {
73 | return $this->sortByDesc(
74 | fn (Favicon $favicon): int => strlen($favicon->content())
75 | )->first();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Concerns/BuildsCacheKeys.php:
--------------------------------------------------------------------------------
1 | buildCacheKey($url).'.collection';
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Concerns/HasDefaultFunctionality.php:
--------------------------------------------------------------------------------
1 | fetch($url)) {
56 | return $favicon;
57 | }
58 |
59 | return $default instanceof \Closure ? $default($url) : $default;
60 | }
61 |
62 | /**
63 | * Attempt to fetch all the favicons for the given URL. If the favicons cannot
64 | * be found, return the default as a fallback.
65 | *
66 | * @param string $url
67 | * @param mixed $default
68 | * @return mixed
69 | *
70 | * @throws FaviconNotFoundException
71 | * @throws InvalidUrlException
72 | * @throws FeatureNotSupportedException
73 | */
74 | public function fetchAllOr(string $url, mixed $default): mixed
75 | {
76 | $favicons = $this->fetchAll($url);
77 |
78 | if ($favicons->isNotEmpty()) {
79 | return $favicons;
80 | }
81 |
82 | return $default instanceof \Closure ? $default($url) : $default;
83 | }
84 |
85 | /**
86 | * Specify whether to throw an exception if the favicon cannot be found.
87 | *
88 | * @param bool $throw
89 | * @return $this
90 | */
91 | public function throw(bool $throw = true): self
92 | {
93 | $this->throwOnNotFound = $throw;
94 |
95 | return $this;
96 | }
97 |
98 | /**
99 | * Specify which drivers should be used as fallbacks if the current
100 | * driver cannot find the favicon.
101 | *
102 | * @param string ...$fallbacks
103 | * @return $this
104 | */
105 | public function withFallback(string ...$fallbacks): self
106 | {
107 | $this->fallbacks = array_merge($this->fallbacks, $fallbacks);
108 |
109 | return $this;
110 | }
111 |
112 | /**
113 | * Specify whether to attempt to read the favicon from the cache.
114 | *
115 | * @param bool $useCache
116 | * @return $this
117 | */
118 | public function useCache(bool $useCache = true): self
119 | {
120 | $this->useCache = $useCache;
121 |
122 | return $this;
123 | }
124 |
125 | /**
126 | * Handle what happens if the favicon cannot be found using the current
127 | * driver. If any fallbacks are specified, attempt to find a favicon
128 | * using a different driver. If we have specified to throw an
129 | * exception, then do so. Otherwise, return null.
130 | *
131 | * @param string $url
132 | * @return FetchedFavicon|null
133 | *
134 | * @throws FaviconNotFoundException
135 | */
136 | protected function notFound(string $url)
137 | {
138 | if ($favicon = $this->attemptFallbacks($url)) {
139 | return $favicon;
140 | }
141 |
142 | if ($this->throwOnNotFound) {
143 | throw new FaviconNotFoundException('A favicon cannot be found for '.$url);
144 | }
145 |
146 | return null;
147 | }
148 |
149 | /**
150 | * Loop through each fallback driver and attempt to retrieve a favicon.
151 | *
152 | * @param string $url
153 | * @return FetchedFavicon|null
154 | */
155 | protected function attemptFallbacks(string $url): ?FetchedFavicon
156 | {
157 | foreach ($this->fallbacks as $driver) {
158 | if ($favicon = Favicon::driver($driver)->fetch($url)) {
159 | return $favicon;
160 | }
161 | }
162 |
163 | return null;
164 | }
165 |
166 | /**
167 | * Return the cached favicon, if one exists, or return null.
168 | *
169 | * @param string $url
170 | * @return FetchedFavicon|null
171 | *
172 | * @throws FaviconFetcherException
173 | */
174 | protected function attemptToFetchFromCache(string $url): ?FetchedFavicon
175 | {
176 | $cachedFaviconData = Cache::get($this->buildCacheKey($url));
177 |
178 | if (! $cachedFaviconData) {
179 | return null;
180 | }
181 |
182 | // If the cached data is still stored in the older format used in
183 | // v1 of the package, then we convert it to the new format. In
184 | // v3 of the package, we will remove this check and enforce
185 | // an array to be stored.
186 | if (is_string($cachedFaviconData)) {
187 | $cachedFaviconData = [
188 | 'favicon_url' => $cachedFaviconData,
189 | 'icon_type' => FetchedFavicon::TYPE_ICON_UNKNOWN,
190 | 'icon_size' => null,
191 | ];
192 | }
193 |
194 | return (new FetchedFavicon(
195 | url: $url,
196 | faviconUrl: $cachedFaviconData['favicon_url'],
197 | retrievedFromCache: true,
198 | ))
199 | ->setIconType($cachedFaviconData['icon_type'])
200 | ->setIconSize($cachedFaviconData['icon_size']);
201 | }
202 |
203 | /**
204 | * Return a collection of cached favicons if they exist, or return null.
205 | *
206 | * @param string $url
207 | * @return FaviconCollection|null
208 | *
209 | * @throws FaviconFetcherException
210 | */
211 | protected function attemptToFetchCollectionFromCache(string $url): ?FaviconCollection
212 | {
213 | $cachedFaviconsData = Cache::get($this->buildCacheKeyForCollection($url));
214 |
215 | if (! $cachedFaviconsData) {
216 | return null;
217 | }
218 |
219 | $favicons = new FaviconCollection();
220 |
221 | foreach ($cachedFaviconsData as $cachedFaviconData) {
222 | $favicons->push((new FetchedFavicon(
223 | url: $url,
224 | faviconUrl: $cachedFaviconData['favicon_url'],
225 | retrievedFromCache: true,
226 | ))
227 | ->setIconType($cachedFaviconData['icon_type'])
228 | ->setIconSize($cachedFaviconData['icon_size']));
229 | }
230 |
231 | return $favicons;
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/src/Concerns/MakesHttpRequests.php:
--------------------------------------------------------------------------------
1 | connectTimeout(config('favicon-fetcher.connect_timeout'));
18 |
19 | if ($userAgent = config('favicon-fetcher.user_agent')) {
20 | $client->withUserAgent($userAgent);
21 | }
22 |
23 | if (! config('favicon-fetcher.verify_tls')) {
24 | $client->withoutVerifying();
25 | }
26 |
27 | return $client;
28 | }
29 |
30 | protected function withRequestExceptionHandling(\Closure $callback): mixed
31 | {
32 | try {
33 | return $callback();
34 | } catch (ClientConnectionException $exception) {
35 | throw new ConnectionException(
36 | $exception->getMessage(),
37 | $exception->getCode(),
38 | $exception
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Concerns/ValidatesUrls.php:
--------------------------------------------------------------------------------
1 | urlIsValid($url)) {
42 | throw new InvalidUrlException($url.' is not a valid URL');
43 | }
44 |
45 | if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
46 | return $favicon;
47 | }
48 |
49 | $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);
50 |
51 | $apiUrl = self::BASE_URL.$urlWithoutProtocol;
52 |
53 | $response = $this->withRequestExceptionHandling(
54 | fn (): Response => $this->httpClient()->get($apiUrl)
55 | );
56 |
57 | if (! $response->successful() || count($response->json('icons')) === 0) {
58 | return $this->notFound($url);
59 | }
60 |
61 | $faviconUrl = $response->json('icons')[0]['src'];
62 |
63 | return new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this);
64 | }
65 |
66 | public function fetchAll(string $url): FaviconCollection
67 | {
68 | throw new FeatureNotSupportedException('The FaviconGrabber driver does not support fetching all favicons.');
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Drivers/FaviconKitDriver.php:
--------------------------------------------------------------------------------
1 | urlIsValid($url)) {
40 | throw new InvalidUrlException($url.' is not a valid URL');
41 | }
42 |
43 | if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
44 | return $favicon;
45 | }
46 |
47 | $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);
48 |
49 | $faviconUrl = self::BASE_URL.$urlWithoutProtocol;
50 |
51 | $response = $this->withRequestExceptionHandling(
52 | fn (): Response => $this->httpClient()->get($faviconUrl)
53 | );
54 |
55 | return $response->successful()
56 | ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
57 | : $this->notFound($url);
58 | }
59 |
60 | public function fetchAll(string $url): FaviconCollection
61 | {
62 | throw new FeatureNotSupportedException('The FaviconKit API does not support fetching all favicons.');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Drivers/GoogleSharedStuffDriver.php:
--------------------------------------------------------------------------------
1 | urlIsValid($url)) {
42 | throw new InvalidUrlException($url.' is not a valid URL');
43 | }
44 |
45 | if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
46 | return $favicon;
47 | }
48 |
49 | $faviconUrl = self::BASE_URL.$url;
50 |
51 | $response = $this->withRequestExceptionHandling(
52 | fn (): Response => $this->httpClient()->get($faviconUrl)
53 | );
54 |
55 | return $response->successful()
56 | ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
57 | : $this->notFound($url);
58 | }
59 |
60 | public function fetchAll(string $url): FaviconCollection
61 | {
62 | throw new FeatureNotSupportedException('The Google Shared Stuff API does not support fetching all favicons.');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Drivers/HttpDriver.php:
--------------------------------------------------------------------------------
1 | urlIsValid($url)) {
43 | throw new InvalidUrlException($url.' is not a valid URL');
44 | }
45 |
46 | if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
47 | return $favicon;
48 | }
49 |
50 | $favicon = $this->attemptToResolveFromHeadTags($url)
51 | ?? new Favicon(url: $url, faviconUrl: $this->guessDefaultUrl($url), fromDriver: $this);
52 |
53 | $faviconCanBeReached = $this->faviconUrlCanBeReached($favicon->getFaviconUrl());
54 |
55 | return $faviconCanBeReached
56 | ? $favicon
57 | : $this->notFound($url);
58 | }
59 |
60 | public function fetchAll(string $url): FaviconCollection
61 | {
62 | if (! $this->urlIsValid($url)) {
63 | throw new InvalidUrlException($url.' is not a valid URL');
64 | }
65 |
66 | if ($this->useCache && $favicons = $this->attemptToFetchCollectionFromCache($url)) {
67 | return $favicons;
68 | }
69 |
70 | $favicons = $this->attemptToResolveAllFromHeadTags($url);
71 |
72 | // If the URL couldn't be reached, throw and exception and return
73 | // an empty FaviconCollection.
74 | if ($favicons === null) {
75 | if ($this->throwOnNotFound) {
76 | throw new FaviconNotFoundException('A favicon cannot be found for '.$url);
77 | }
78 |
79 | $favicons = new FaviconCollection();
80 | }
81 |
82 | if ($favicons->isEmpty()) {
83 | $favicons->push(new Favicon(url: $url, faviconUrl: $this->guessDefaultUrl($url), fromDriver: $this));
84 | }
85 |
86 | // Return a FaviconCollection of favicons that can be reached.
87 | return $favicons->filter(
88 | fn (Favicon $favicon): bool => $this->faviconUrlCanBeReached($favicon->getFaviconUrl())
89 | );
90 | }
91 |
92 | /**
93 | * Attempt to resolve a favicon from the given URL. If the response
94 | * is successful, we can assume that a valid favicon was returned.
95 | * Otherwise, we can assume that a favicon wasn't found.
96 | *
97 | * @param string $faviconUrl
98 | * @return bool
99 | *
100 | * @throws ConnectionException
101 | */
102 | private function faviconUrlCanBeReached(string $faviconUrl): bool
103 | {
104 | return $this->withRequestExceptionHandling(
105 | fn (): bool => $this->httpClient()
106 | ->get($faviconUrl)
107 | ->successful()
108 | );
109 | }
110 |
111 | /**
112 | * Parse the HTML returned from the URL and attempt to find a favicon
113 | * specified using the "icon" or "shortcut icon" link tag. If one
114 | * is found, return the absolute URL of the link's "href".
115 | * Otherwise, return null.
116 | *
117 | * @param string $url
118 | * @return Favicon|null
119 | *
120 | * @throws InvalidIconSizeException
121 | * @throws InvalidIconTypeException
122 | * @throws ConnectionException
123 | */
124 | private function attemptToResolveFromHeadTags(string $url): ?Favicon
125 | {
126 | $response = $this->withRequestExceptionHandling(
127 | fn (): Response => $this->httpClient()->get($url)
128 | );
129 |
130 | if (! $response->successful()) {
131 | return null;
132 | }
133 |
134 | $linkTag = (new Crawler($response->body()))
135 | ->filter('
136 | head link[rel="icon"][href],
137 | head link[rel="shortcut icon"][href]
138 | ')
139 | ->first();
140 |
141 | if (! $linkTag->count()) {
142 | return null;
143 | }
144 |
145 | $favicon = new Favicon(
146 | url: $url,
147 | faviconUrl: $this->convertToAbsoluteUrl($url, $linkTag->attr('href')),
148 | fromDriver: $this,
149 | );
150 |
151 | if ($iconSize = $linkTag->attr('sizes')) {
152 | $favicon->setIconSize((int) $iconSize);
153 | }
154 |
155 | if ($iconType = $this->guessTypeFromElement($linkTag)) {
156 | $favicon->setIconType($iconType);
157 | }
158 |
159 | return $favicon;
160 | }
161 |
162 | /**
163 | * @throws ConnectionException
164 | */
165 | private function attemptToResolveAllFromHeadTags(string $url): ?FaviconCollection
166 | {
167 | $response = $this->withRequestExceptionHandling(
168 | fn (): Response => $this->httpClient()->get($url)
169 | );
170 |
171 | if (! $response->successful()) {
172 | return null;
173 | }
174 |
175 | $linkTags = (new Crawler($response->body()))
176 | ->filter('
177 | head link[rel="icon"][href],
178 | head link[rel="shortcut icon"][href],
179 | head link[rel="apple-touch-icon"][href]
180 | ');
181 |
182 | if (! $linkTags->count()) {
183 | return null;
184 | }
185 |
186 | $favicons = $linkTags->each(function (Crawler $linkTag) use ($url): Favicon {
187 | $favicon = new Favicon(
188 | $url,
189 | $this->convertToAbsoluteUrl($url, $linkTag->attr('href')),
190 | $this,
191 | );
192 |
193 | if ($iconSize = $linkTag->attr('sizes')) {
194 | $favicon->setIconSize((int) $iconSize);
195 | }
196 |
197 | if ($iconType = $this->guessTypeFromElement($linkTag)) {
198 | $favicon->setIconType($iconType);
199 | }
200 |
201 | return $favicon;
202 | });
203 |
204 | return new FaviconCollection($favicons);
205 | }
206 |
207 | private function guessTypeFromElement(Crawler $linkElement): string
208 | {
209 | return match ($linkElement->attr('rel')) {
210 | 'icon' => Favicon::TYPE_ICON,
211 | 'shortcut icon' => Favicon::TYPE_SHORTCUT_ICON,
212 | 'apple-touch-icon' => Favicon::TYPE_APPLE_TOUCH_ICON,
213 | default => Favicon::TYPE_ICON_UNKNOWN,
214 | };
215 | }
216 |
217 | /**
218 | * Convert the favicon URL to be absolute rather than relative.
219 | *
220 | * @param string $baseUrl
221 | * @param string $faviconUrl
222 | * @return string
223 | */
224 | private function convertToAbsoluteUrl(string $baseUrl, string $faviconUrl): string
225 | {
226 | // If the favicon URL is relative, we need to convert it to be absolute.
227 | // We also strip the path (if there is one) from the base URL.
228 | if (! filter_var($faviconUrl, FILTER_VALIDATE_URL)) {
229 | $faviconUrl = $this->stripPathFromUrl($baseUrl).'/'.ltrim($faviconUrl, '/');
230 | }
231 |
232 | return $faviconUrl;
233 | }
234 |
235 | /**
236 | * Build and return the default path where we can guess the favicon
237 | * file might be stored.
238 | *
239 | * @param string $url
240 | * @return string
241 | */
242 | private function guessDefaultUrl(string $url): string
243 | {
244 | return rtrim($this->stripPathFromUrl($url)).'/favicon.ico';
245 | }
246 |
247 | /**
248 | * Strip the path and any query parameters from the given URL so that
249 | * we only return the scheme, host and port (if there is one).
250 | *
251 | * @param string $url
252 | * @return string
253 | */
254 | private function stripPathFromUrl(string $url): string
255 | {
256 | $parsedUrl = parse_url($url);
257 |
258 | $url = $parsedUrl['scheme'].'://'.$parsedUrl['host'];
259 |
260 | if (array_key_exists('port', $parsedUrl)) {
261 | $url .= ':'.$parsedUrl['port'];
262 | }
263 |
264 | return $url;
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/Drivers/UnavatarDriver.php:
--------------------------------------------------------------------------------
1 | urlIsValid($url)) {
40 | throw new InvalidUrlException($url.' is not a valid URL');
41 | }
42 |
43 | if ($this->useCache && $favicon = $this->attemptToFetchFromCache($url)) {
44 | return $favicon;
45 | }
46 |
47 | $urlWithoutProtocol = str_replace(['https://', 'http://'], '', $url);
48 |
49 | $faviconUrl = self::BASE_URL.$urlWithoutProtocol.'?fallback=false';
50 |
51 | $response = $this->withRequestExceptionHandling(
52 | fn (): Response => $this->httpClient()->get($faviconUrl)
53 | );
54 |
55 | return $response->successful()
56 | ? new Favicon(url: $url, faviconUrl: $faviconUrl, fromDriver: $this)
57 | : $this->notFound($url);
58 | }
59 |
60 | public function fetchAll(string $url): FaviconCollection
61 | {
62 | throw new FeatureNotSupportedException('The Unavatar API does not support fetching all favicons.');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Exceptions/ConnectionException.php:
--------------------------------------------------------------------------------
1 | url = $url;
73 | $this->faviconUrl = $faviconUrl;
74 | $this->driver = $fromDriver;
75 | $this->retrievedFromCache = $retrievedFromCache;
76 | }
77 |
78 | public function setIconSize(?int $size): static
79 | {
80 | if ($size !== null && $size < 0) {
81 | throw new InvalidIconSizeException('The size ['.$size.'] is not a valid favicon size.');
82 | }
83 |
84 | $this->size = $size;
85 |
86 | return $this;
87 | }
88 |
89 | public function setIconType(string $type): static
90 | {
91 | if (! $this->acceptableIconType($type)) {
92 | throw new InvalidIconTypeException('The type ['.$type.'] is not a valid favicon type.');
93 | }
94 |
95 | $this->iconType = $type;
96 |
97 | return $this;
98 | }
99 |
100 | public function getUrl(): string
101 | {
102 | return $this->url;
103 | }
104 |
105 | public function getFaviconUrl(): string
106 | {
107 | return $this->faviconUrl;
108 | }
109 |
110 | public function retrievedFromCache(): bool
111 | {
112 | return $this->retrievedFromCache;
113 | }
114 |
115 | /**
116 | * Get the contents of the favicon file.
117 | *
118 | * @return string
119 | *
120 | * @throws ConnectionException
121 | */
122 | public function content(): string
123 | {
124 | return $this->withRequestExceptionHandling(
125 | fn (): Response => $this->httpClient()->get($this->faviconUrl)
126 | )->body();
127 | }
128 |
129 | /**
130 | * Cache the favicon URL. If the favicon is already cached, "force"
131 | * must be passed as "true" to re-cache the URL.
132 | *
133 | * @param CarbonInterface $ttl
134 | * @param bool $force
135 | * @return $this
136 | */
137 | public function cache(CarbonInterface $ttl, bool $force = false): self
138 | {
139 | if ($force || ! $this->retrievedFromCache) {
140 | Cache::put(
141 | $this->buildCacheKey($this->url),
142 | $this->toCache(),
143 | $ttl
144 | );
145 | }
146 |
147 | return $this;
148 | }
149 |
150 | /**
151 | * Store the favicon in storage using an automatically generate filename.
152 | *
153 | * @param string $directory
154 | * @param string|null $disk
155 | * @return string
156 | */
157 | public function store(string $directory, ?string $disk = null): string
158 | {
159 | return $this->storeAs($directory, Str::uuid()->toString(), $disk);
160 | }
161 |
162 | /**
163 | * Store the favicon in storage.
164 | *
165 | * @param string $directory
166 | * @param string $filename
167 | * @param string|null $disk
168 | * @return string
169 | */
170 | public function storeAs(string $directory, string $filename, ?string $disk = null): string
171 | {
172 | $path = $this->buildStoragePath($directory, $filename);
173 |
174 | Storage::disk($disk)->put($path, $this->content());
175 |
176 | return $path;
177 | }
178 |
179 | public function getIconType(): string
180 | {
181 | return $this->iconType;
182 | }
183 |
184 | public function getIconSize(): ?int
185 | {
186 | return $this->size;
187 | }
188 |
189 | protected function buildStoragePath(string $directory, string $filename): string
190 | {
191 | return Str::of($directory)
192 | ->append('/')
193 | ->append($filename)
194 | ->append('.')
195 | ->append($this->guessFileExtension())
196 | ->toString();
197 | }
198 |
199 | protected function guessFileExtension(): string
200 | {
201 | $default = File::extension($this->faviconUrl);
202 |
203 | if (Str::of($this->faviconUrl)->endsWith(['png', 'ico', 'svg'])) {
204 | return $default;
205 | }
206 |
207 | return $this->guessFileExtensionFromMimeType() ?? $default;
208 | }
209 |
210 | /**
211 | * @throws ConnectionException
212 | */
213 | protected function guessFileExtensionFromMimeType(): ?string
214 | {
215 | $faviconMimetype = $this->withRequestExceptionHandling(
216 | fn (): Response => $this->httpClient()->get($this->faviconUrl)
217 | )->header('content-type');
218 |
219 | $mimeToExtensionMap = [
220 | 'image/x-icon' => 'ico',
221 | 'image/x-ico' => 'ico',
222 | 'image/vnd.microsoft.icon' => 'ico',
223 | 'image/jpeg' => 'jpeg',
224 | 'image/pjpeg' => 'jpeg',
225 | 'image/png' => 'png',
226 | 'image/x-png' => 'png',
227 | 'image/svg+xml' => 'svg',
228 | ];
229 |
230 | return $mimeToExtensionMap[$faviconMimetype] ?? null;
231 | }
232 |
233 | private function acceptableIconType(string $type): bool
234 | {
235 | return in_array(
236 | needle: $type,
237 | haystack: [
238 | self::TYPE_ICON,
239 | self::TYPE_SHORTCUT_ICON,
240 | self::TYPE_APPLE_TOUCH_ICON,
241 | self::TYPE_ICON_UNKNOWN,
242 | ],
243 | strict: true);
244 | }
245 |
246 | /**
247 | * Transform the favicon object into an array that can be cached.
248 | *
249 | * @return array
250 | */
251 | public function toCache(): array
252 | {
253 | return [
254 | 'favicon_url' => $this->getFaviconUrl(),
255 | 'icon_size' => $this->getIconSize(),
256 | 'icon_type' => $this->getIconType(),
257 | ];
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/FaviconFetcherProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__.'/../config/favicon-fetcher.php', 'favicon-fetcher');
19 |
20 | $this->app->bind('favicon-fetcher', fn () => new FetcherManager());
21 | }
22 |
23 | /**
24 | * Bootstrap any application services.
25 | *
26 | * @return void
27 | */
28 | public function boot(): void
29 | {
30 | $this->publishes([
31 | __DIR__.'/../config/favicon-fetcher.php' => config_path('favicon-fetcher.php'),
32 | ], 'favicon-fetcher-config');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/FetcherManager.php:
--------------------------------------------------------------------------------
1 | new HttpDriver(),
28 | 'google-shared-stuff' => new GoogleSharedStuffDriver(),
29 | 'favicon-kit' => new FaviconKitDriver(),
30 | 'unavatar' => new UnavatarDriver(),
31 | 'favicon-grabber' => new FaviconGrabberDriver(),
32 | default => static::attemptToCreateCustomDriver($driver),
33 | };
34 | }
35 |
36 | public static function extend(string $name, Fetcher $fetcher): void
37 | {
38 | self::$customDrivers[$name] = $fetcher;
39 | }
40 |
41 | protected static function attemptToCreateCustomDriver(string $driver): Fetcher
42 | {
43 | return static::$customDrivers[$driver]
44 | ?? throw new FaviconFetcherException($driver.' is not a valid driver.');
45 | }
46 |
47 | public function __call(string $method, mixed $parameters): mixed
48 | {
49 | return static::driver()->$method(...$parameters);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Feature/Collections/FaviconCollectionTest.php:
--------------------------------------------------------------------------------
1 | setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
22 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
23 | ]);
24 |
25 | $collection->cache(now()->addDay());
26 |
27 | $cachedItems = Cache::get('favicon-fetcher.example.com.collection');
28 |
29 | self::assertSame(
30 | expected: [
31 | [
32 | 'favicon_url' => 'https://example.com/images/apple-icon-180x180.png',
33 | 'icon_size' => 180,
34 | 'icon_type' => 'apple_touch_icon',
35 | ],
36 | [
37 | 'favicon_url' => 'https://example.com/images/favicon.ico',
38 | 'icon_size' => null,
39 | 'icon_type' => 'shortcut_icon',
40 | ],
41 | ],
42 | actual: $cachedItems
43 | );
44 | }
45 |
46 | /** @test */
47 | public function favicon_collection_can_be_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_true(): void
48 | {
49 | Cache::put(
50 | key: 'favicon-fetcher.example.com.collection',
51 | value: 'Dummy value here that should be overridden',
52 | ttl: now()->addDay(),
53 | );
54 |
55 | FaviconCollection::makeFromCache([
56 | (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
57 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
58 | ])->cache(now()->addDay(), true);
59 |
60 | // Assert that the items in the database were overridden.
61 | self::assertSame(
62 | expected: [
63 | [
64 | 'favicon_url' => 'https://example.com/images/apple-icon-180x180.png',
65 | 'icon_size' => 180,
66 | 'icon_type' => 'apple_touch_icon',
67 | ],
68 | [
69 | 'favicon_url' => 'https://example.com/images/favicon.ico',
70 | 'icon_size' => null,
71 | 'icon_type' => 'shortcut_icon',
72 | ],
73 | ],
74 | actual: Cache::get('favicon-fetcher.example.com.collection')
75 | );
76 | }
77 |
78 | /** @test */
79 | public function favicon_collection_is_not_cached_if_the_collection_was_retrieved_from_the_cache_and_the_force_flag_is_false(): void
80 | {
81 | Cache::put(
82 | key: 'favicon-fetcher.example.com.collection',
83 | value: 'Dummy value here that should not be overridden',
84 | ttl: now()->addDay(),
85 | );
86 |
87 | FaviconCollection::makeFromCache([
88 | (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
89 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
90 | ])->cache(now()->addDay());
91 |
92 | // Assert that the items in the database were not overridden.
93 | self::assertSame(
94 | expected: 'Dummy value here that should not be overridden',
95 | actual: Cache::get('favicon-fetcher.example.com.collection')
96 | );
97 | }
98 |
99 | /** @test */
100 | public function favicon_collection_is_not_cached_if_the_collection_is_empty(): void
101 | {
102 | Cache::shouldReceive('put')->never();
103 |
104 | $collection = new FaviconCollection();
105 |
106 | $collection->cache(now()->addDay());
107 | }
108 |
109 | /** @test */
110 | public function largest_favicon_can_be_retrieved(): void
111 | {
112 | $largest = FaviconCollection::make([
113 | (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
114 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-57x57.png'))->setIconSize(57)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
115 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-60x60.png'))->setIconSize(60)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
116 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(72)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
117 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(76)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
118 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-76x76.png'))->setIconSize(114)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
119 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-120x120.png'))->setIconSize(120)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
120 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-144x144.png'))->setIconSize(144)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
121 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-152x152.png'))->setIconSize(152)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
122 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
123 | (new Favicon('https://example.com', 'https://example.com/favicon/android-icon-192x192.png'))->setIconSize(192)->setIconType(Favicon::TYPE_ICON),
124 | ])->largest();
125 |
126 | self::assertSame('https://example.com/favicon/android-icon-192x192.png', $largest->getFaviconUrl());
127 | }
128 |
129 | /** @test */
130 | public function largest_favicon_can_be_retrieved_if_there_are_only_null_sizes(): void
131 | {
132 | $largest = FaviconCollection::make([
133 | (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
134 | (new Favicon('https://example.com', 'https://example.com/favicon/favicon-64x64.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
135 | ])->largest();
136 |
137 | self::assertSame('https://example.com/favicon/favicon-32x32.png', $largest->getFaviconUrl());
138 | }
139 |
140 | /** @test */
141 | public function largest_favicon_can_be_retrieved_based_on_file_size()
142 | {
143 | // mock the favicons to specify file content lengths
144 | $favicon1 = $this->createMock(Favicon::class);
145 | $favicon1->method('getFaviconUrl')->willReturn('https://example.com/favicon/favicon-32x32.png');
146 | $favicon1->method('content')->willReturn('some-short-string');
147 |
148 | $favicon2 = $this->createMock(Favicon::class);
149 | $favicon2->method('getFaviconUrl')->willReturn('https://example.com/favicon/favicon-64x64.png');
150 | $favicon2->method('content')->willReturn('some-much-longer-string');
151 |
152 | $largest = FaviconCollection::make([
153 | $favicon1,
154 | $favicon2,
155 | ])->largestByFileSize();
156 |
157 | self::assertSame('https://example.com/favicon/favicon-64x64.png', $largest->getFaviconUrl());
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/tests/Feature/Concerns/MakesHttpRequests/HttpClientTest.php:
--------------------------------------------------------------------------------
1 | 10,
19 | 'favicon-fetcher.connect_timeout' => 5,
20 | 'favicon-fetcher.verify_tls' => false,
21 | ]);
22 |
23 | $client = $this->httpClient();
24 |
25 | self::assertEquals(10, $client->getOptions()['timeout']);
26 | self::assertEquals(5, $client->getOptions()['connect_timeout']);
27 | self::assertFalse($client->getOptions()['verify']);
28 | }
29 |
30 | /** @test */
31 | public function http_client_is_returned_with_correct_verify_tls_option(): void
32 | {
33 | config([
34 | 'favicon-fetcher.verify_tls' => true,
35 | ]);
36 |
37 | $client = $this->httpClient();
38 |
39 | // The "verify" option shouldn't be present because we've not set it,
40 | // so if it's not in the array, we can make the assumption that it's
41 | // set to true under the hood by Laravel.
42 | self::assertArrayNotHasKey('verify', $client->getOptions());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Feature/Concerns/MakesHttpRequests/WithRequestExceptionHandlingTest.php:
--------------------------------------------------------------------------------
1 | expectException(ConnectionException::class);
20 |
21 | $this->withRequestExceptionHandling(function () {
22 | throw new ClientConnectionException('Test exception');
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Feature/Drivers/FaviconGrabberDriverTest.php:
--------------------------------------------------------------------------------
1 | Http::response($this->successfulResponseBody()),
33 | '*' => Http::response('should not hit here'),
34 | ]);
35 |
36 | $favicon = (new FaviconGrabberDriver())->fetch($protocol.'://aws.amazon.com');
37 |
38 | self::assertSame('https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico', $favicon->getFaviconUrl());
39 | }
40 |
41 | /** @test */
42 | public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
43 | {
44 | Cache::put(
45 | 'favicon-fetcher.aws.amazon.com',
46 | [
47 | 'favicon_url' => 'url-goes-here',
48 | 'icon_size' => null,
49 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
50 | ],
51 | now()->addHour()
52 | );
53 |
54 | Http::fake([
55 | '*' => Http::response('should not hit here'),
56 | ]);
57 |
58 | $favicon = (new FaviconGrabberDriver())->fetch('https://aws.amazon.com');
59 |
60 | self::assertSame('url-goes-here', $favicon->getFaviconUrl());
61 | }
62 |
63 | /** @test */
64 | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
65 | {
66 | Cache::put(
67 | 'favicon-fetcher.https://aws.amazon.com',
68 | 'url-goes-here',
69 | now()->addHour()
70 | );
71 |
72 | Http::fake([
73 | 'https://favicongrabber.com/api/grab/aws.amazon.com' => Http::response($this->successfulResponseBody()),
74 | '*' => Http::response('should not hit here'),
75 | ]);
76 |
77 | $favicon = (new FaviconGrabberDriver())->useCache(false)->fetch('https://aws.amazon.com');
78 |
79 | self::assertSame('https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico', $favicon->getFaviconUrl());
80 | }
81 |
82 | /** @test */
83 | public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
84 | {
85 | Http::fake([
86 | 'https://favicongrabber.com/api/grab/empty.com' => Http::response($this->successfulEmptyResponseBody()),
87 | '*' => Http::response('should not hit here'),
88 | ]);
89 |
90 | $favicon = (new FaviconGrabberDriver())->useCache(true)->fetch('https://empty.com');
91 |
92 | self::assertNull($favicon);
93 | }
94 |
95 | /** @test */
96 | public function null_is_returned_if_the_domain_is_invalid(): void
97 | {
98 | Http::fake([
99 | 'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
100 | '*' => Http::response('should not hit here'),
101 | ]);
102 |
103 | $favicon = (new FaviconGrabberDriver())->useCache(true)->fetch('https://invalid.com');
104 |
105 | self::assertNull($favicon);
106 | }
107 |
108 | /** @test */
109 | public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
110 | {
111 | Http::fake([
112 | 'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
113 | '*' => Http::response('should not hit here'),
114 | ]);
115 |
116 | FetcherManager::extend('custom-driver', new CustomDriver());
117 |
118 | $favicon = (new FaviconGrabberDriver())
119 | ->withFallback('custom-driver')
120 | ->useCache(true)
121 | ->fetch('https://invalid.com');
122 |
123 | self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
124 | }
125 |
126 | /** @test */
127 | public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
128 | {
129 | Http::fake([
130 | 'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
131 | '*' => Http::response('should not hit here'),
132 | ]);
133 |
134 | $exception = null;
135 |
136 | try {
137 | (new FaviconGrabberDriver())
138 | ->throw()
139 | ->useCache(true)
140 | ->fetch('https://invalid.com');
141 | } catch (\Exception $e) {
142 | $exception = $e;
143 | }
144 |
145 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
146 | self::assertSame('A favicon cannot be found for https://invalid.com', $exception->getMessage());
147 | }
148 |
149 | /** @test */
150 | public function default_value_can_be_returned_using_fetchOr_method(): void
151 | {
152 | Http::fake([
153 | 'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
154 | '*' => Http::response('should not hit here'),
155 | ]);
156 |
157 | $favicon = (new FaviconGrabberDriver())
158 | ->useCache(true)
159 | ->fetchOr('https://invalid.com', 'fallback-to-this');
160 |
161 | self::assertSame('fallback-to-this', $favicon);
162 | }
163 |
164 | /** @test */
165 | public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
166 | {
167 | Http::fake([
168 | 'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
169 | '*' => Http::response('should not hit here'),
170 | ]);
171 |
172 | $favicon = (new FaviconGrabberDriver())
173 | ->fetchOr('https://invalid.com', function () {
174 | return 'fallback-to-this';
175 | });
176 |
177 | self::assertSame('fallback-to-this', $favicon);
178 | }
179 |
180 | /** @test */
181 | public function exception_can_be_thrown_after_attempting_a_fallback(): void
182 | {
183 | Http::fake([
184 | 'https://favicongrabber.com/api/grab/invalid.com' => Http::response($this->domainNotFoundResponseBody(), 400),
185 | '*' => Http::response('should not hit here'),
186 | ]);
187 |
188 | FetcherManager::extend('custom-driver', new NullDriver());
189 |
190 | $exception = null;
191 |
192 | try {
193 | (new FaviconGrabberDriver())
194 | ->throw()
195 | ->withFallback('custom-driver')
196 | ->fetch('https://invalid.com');
197 | } catch (\Exception $e) {
198 | $exception = $e;
199 | }
200 |
201 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
202 | self::assertSame('A favicon cannot be found for https://invalid.com', $exception->getMessage());
203 |
204 | self::assertTrue(NullDriver::$flag);
205 | }
206 |
207 | /** @test */
208 | public function exception_is_thrown_if_the_url_is_invalid(): void
209 | {
210 | Http::fake([
211 | '*' => Http::response('should not hit here'),
212 | ]);
213 |
214 | $exception = null;
215 |
216 | try {
217 | (new FaviconGrabberDriver())->fetch('example.com');
218 | } catch (\Exception $e) {
219 | $exception = $e;
220 | }
221 |
222 | self::assertInstanceOf(InvalidUrlException::class, $exception);
223 | self::assertSame('example.com is not a valid URL', $exception->getMessage());
224 | }
225 |
226 | private function successfulResponseBody(): array
227 | {
228 | return [
229 | 'domain' => 'aws.amazon.com',
230 | 'icons' => [
231 | [
232 | 'src' => 'https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico',
233 | 'type' => 'image/ico',
234 | ],
235 | [
236 | 'src' => 'https://a0.awsstatic.com/libra-css/images/site/fav/favicon.ico',
237 | 'type' => 'image/ico',
238 | ],
239 | [
240 | 'sizes' => '57x57',
241 | 'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-iphone-114-smile.png',
242 | ],
243 | [
244 | 'sizes' => '72x72',
245 | 'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-ipad-144-smile.png',
246 | ],
247 | [
248 | 'sizes' => '114x114',
249 | 'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-iphone-114-smile.png',
250 | ],
251 | [
252 | 'sizes' => '144x144',
253 | 'src' => 'https://a0.awsstatic.com/libra-css/images/site/touch-icon-ipad-144-smile.png',
254 | ],
255 | [
256 | 'src' => 'https://aws.amazon.com/favicon.ico',
257 | 'type' => 'image/x-icon',
258 | ],
259 | ],
260 | ];
261 | }
262 |
263 | private function successfulEmptyResponseBody(): array
264 | {
265 | return [
266 | 'domain' => 'empty.com',
267 | 'icons' => [],
268 | ];
269 | }
270 |
271 | private function domainNotFoundResponseBody(): array
272 | {
273 | return [
274 | 'error' => 'Unresolved domain name.',
275 | ];
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/tests/Feature/Drivers/FaviconKitDriverTest.php:
--------------------------------------------------------------------------------
1 | Http::response('favicon contents here'),
33 | '*' => Http::response('should not hit here'),
34 | ]);
35 |
36 | $favicon = (new FaviconKitDriver())->fetch($protocol.'://example.com');
37 |
38 | self::assertSame('https://api.faviconkit.com/example.com', $favicon->getFaviconUrl());
39 | }
40 |
41 | /** @test */
42 | public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
43 | {
44 | Cache::put(
45 | 'favicon-fetcher.example.com',
46 | [
47 | 'favicon_url' => 'url-goes-here',
48 | 'icon_size' => null,
49 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
50 | ],
51 | now()->addHour()
52 | );
53 |
54 | Http::fake([
55 | '*' => Http::response('should not hit here'),
56 | ]);
57 |
58 | $favicon = (new FaviconKitDriver())->fetch('https://example.com');
59 |
60 | self::assertSame('url-goes-here', $favicon->getFaviconUrl());
61 | }
62 |
63 | /** @test */
64 | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
65 | {
66 | Cache::put(
67 | 'favicon-fetcher.https://example.com',
68 | 'url-goes-here',
69 | now()->addHour()
70 | );
71 |
72 | Http::fake([
73 | 'https://api.faviconkit.com/example.com' => Http::response('favicon contents here'),
74 | '*' => Http::response('should not hit here'),
75 | ]);
76 |
77 | $favicon = (new FaviconKitDriver())->useCache(false)->fetch('https://example.com');
78 |
79 | self::assertSame('https://api.faviconkit.com/example.com', $favicon->getFaviconUrl());
80 | }
81 |
82 | /** @test */
83 | public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
84 | {
85 | Http::fake([
86 | 'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
87 | '*' => Http::response('should not hit here'),
88 | ]);
89 |
90 | $favicon = (new FaviconKitDriver())->useCache(true)->fetch('https://example.com');
91 |
92 | self::assertNull($favicon);
93 | }
94 |
95 | /** @test */
96 | public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
97 | {
98 | Http::fake([
99 | 'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
100 | '*' => Http::response('should not hit here'),
101 | ]);
102 |
103 | FetcherManager::extend('custom-driver', new CustomDriver());
104 |
105 | $favicon = (new FaviconKitDriver())
106 | ->withFallback('custom-driver')
107 | ->useCache(true)
108 | ->fetch('https://example.com');
109 |
110 | self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
111 | }
112 |
113 | /** @test */
114 | public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
115 | {
116 | Http::fake([
117 | 'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
118 | '*' => Http::response('should not hit here'),
119 | ]);
120 |
121 | $exception = null;
122 |
123 | try {
124 | (new FaviconKitDriver())
125 | ->throw()
126 | ->useCache(true)
127 | ->fetch('https://example.com');
128 | } catch (\Exception $e) {
129 | $exception = $e;
130 | }
131 |
132 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
133 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
134 | }
135 |
136 | /** @test */
137 | public function default_value_can_be_returned_using_fetchOr_method(): void
138 | {
139 | Http::fake([
140 | 'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
141 | '*' => Http::response('should not hit here'),
142 | ]);
143 |
144 | $favicon = (new FaviconKitDriver())
145 | ->useCache(true)
146 | ->fetchOr('https://example.com', 'fallback-to-this');
147 |
148 | self::assertSame('fallback-to-this', $favicon);
149 | }
150 |
151 | /** @test */
152 | public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
153 | {
154 | Http::fake([
155 | 'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
156 | '*' => Http::response('should not hit here'),
157 | ]);
158 |
159 | $favicon = (new FaviconKitDriver())
160 | ->fetchOr('https://example.com', function () {
161 | return 'fallback-to-this';
162 | });
163 |
164 | self::assertSame('fallback-to-this', $favicon);
165 | }
166 |
167 | /** @test */
168 | public function exception_can_be_thrown_after_attempting_a_fallback(): void
169 | {
170 | Http::fake([
171 | 'https://api.faviconkit.com/example.com' => Http::response('not found', 404),
172 | '*' => Http::response('should not hit here'),
173 | ]);
174 |
175 | FetcherManager::extend('custom-driver', new NullDriver());
176 |
177 | $exception = null;
178 |
179 | try {
180 | (new FaviconKitDriver())
181 | ->throw()
182 | ->withFallback('custom-driver')
183 | ->fetch('https://example.com');
184 | } catch (\Exception $e) {
185 | $exception = $e;
186 | }
187 |
188 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
189 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
190 |
191 | self::assertTrue(NullDriver::$flag);
192 | }
193 |
194 | /** @test */
195 | public function exception_is_thrown_if_the_url_is_invalid(): void
196 | {
197 | Http::fake([
198 | '*' => Http::response('should not hit here'),
199 | ]);
200 |
201 | $exception = null;
202 |
203 | try {
204 | (new FaviconKitDriver())->fetch('example.com');
205 | } catch (\Exception $e) {
206 | $exception = $e;
207 | }
208 |
209 | self::assertInstanceOf(InvalidUrlException::class, $exception);
210 | self::assertSame('example.com is not a valid URL', $exception->getMessage());
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/tests/Feature/Drivers/GoogleSharedStuffDriverTest.php:
--------------------------------------------------------------------------------
1 | Http::response('favicon contents here'),
33 | '*' => Http::response('should not hit here'),
34 | ]);
35 |
36 | $favicon = (new GoogleSharedStuffDriver())->fetch($protocol.'://example.com');
37 |
38 | self::assertSame('https://www.google.com/s2/favicons?domain='.$protocol.'://example.com', $favicon->getFaviconUrl());
39 | }
40 |
41 | /** @test */
42 | public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
43 | {
44 | Cache::put(
45 | 'favicon-fetcher.example.com',
46 | [
47 | 'favicon_url' => 'url-goes-here',
48 | 'icon_size' => null,
49 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
50 | ],
51 | now()->addHour()
52 | );
53 |
54 | Http::fake([
55 | '*' => Http::response('should not hit here'),
56 | ]);
57 |
58 | $favicon = (new GoogleSharedStuffDriver())->fetch('https://example.com');
59 |
60 | self::assertSame('url-goes-here', $favicon->getFaviconUrl());
61 | }
62 |
63 | /** @test */
64 | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
65 | {
66 | Cache::put(
67 | 'favicon-fetcher.https://example.com',
68 | 'url-goes-here',
69 | now()->addHour()
70 | );
71 |
72 | Http::fake([
73 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('favicon contents here'),
74 | '*' => Http::response('should not hit here'),
75 | ]);
76 |
77 | $favicon = (new GoogleSharedStuffDriver())->useCache(false)->fetch('https://example.com');
78 |
79 | self::assertSame('https://www.google.com/s2/favicons?domain=https://example.com', $favicon->getFaviconUrl());
80 | }
81 |
82 | /** @test */
83 | public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
84 | {
85 | Http::fake([
86 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
87 | '*' => Http::response('should not hit here'),
88 | ]);
89 |
90 | $favicon = (new GoogleSharedStuffDriver())->useCache(true)->fetch('https://example.com');
91 |
92 | self::assertNull($favicon);
93 | }
94 |
95 | /** @test */
96 | public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
97 | {
98 | Http::fake([
99 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
100 | '*' => Http::response('should not hit here'),
101 | ]);
102 |
103 | FetcherManager::extend('custom-driver', new CustomDriver());
104 |
105 | $favicon = (new GoogleSharedStuffDriver())
106 | ->withFallback('custom-driver')
107 | ->useCache(true)
108 | ->fetch('https://example.com');
109 |
110 | self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
111 | }
112 |
113 | /** @test */
114 | public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
115 | {
116 | Http::fake([
117 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
118 | '*' => Http::response('should not hit here'),
119 | ]);
120 |
121 | $exception = null;
122 |
123 | try {
124 | (new GoogleSharedStuffDriver())
125 | ->throw()
126 | ->useCache(true)
127 | ->fetch('https://example.com');
128 | } catch (\Exception $e) {
129 | $exception = $e;
130 | }
131 |
132 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
133 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
134 | }
135 |
136 | /** @test */
137 | public function default_value_can_be_returned_using_fetchOr_method(): void
138 | {
139 | Http::fake([
140 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
141 | '*' => Http::response('should not hit here'),
142 | ]);
143 |
144 | $favicon = (new GoogleSharedStuffDriver())
145 | ->useCache(true)
146 | ->fetchOr('https://example.com', 'fallback-to-this');
147 |
148 | self::assertSame('fallback-to-this', $favicon);
149 | }
150 |
151 | /** @test */
152 | public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
153 | {
154 | Http::fake([
155 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
156 | '*' => Http::response('should not hit here'),
157 | ]);
158 |
159 | $favicon = (new GoogleSharedStuffDriver())
160 | ->fetchOr('https://example.com', function () {
161 | return 'fallback-to-this';
162 | });
163 |
164 | self::assertSame('fallback-to-this', $favicon);
165 | }
166 |
167 | /** @test */
168 | public function exception_can_be_thrown_after_attempting_a_fallback(): void
169 | {
170 | Http::fake([
171 | 'https://www.google.com/s2/favicons?domain=https://example.com' => Http::response('not found', 404),
172 | '*' => Http::response('should not hit here'),
173 | ]);
174 |
175 | FetcherManager::extend('custom-driver', new NullDriver());
176 |
177 | $exception = null;
178 |
179 | try {
180 | (new GoogleSharedStuffDriver())
181 | ->throw()
182 | ->withFallback('custom-driver')
183 | ->fetch('https://example.com');
184 | } catch (\Exception $e) {
185 | $exception = $e;
186 | }
187 |
188 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
189 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
190 |
191 | self::assertTrue(NullDriver::$flag);
192 | }
193 |
194 | /** @test */
195 | public function exception_is_thrown_if_the_url_is_invalid(): void
196 | {
197 | Http::fake([
198 | '*' => Http::response('should not hit here'),
199 | ]);
200 |
201 | $exception = null;
202 |
203 | try {
204 | (new GoogleSharedStuffDriver())->fetch('example.com');
205 | } catch (\Exception $e) {
206 | $exception = $e;
207 | }
208 |
209 | self::assertInstanceOf(InvalidUrlException::class, $exception);
210 | self::assertSame('example.com is not a valid URL', $exception->getMessage());
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/tests/Feature/Drivers/HttpDriverTest.php:
--------------------------------------------------------------------------------
1 | Http::response($html),
40 | $expectedFaviconUrl => Http::response('favicon contents here'),
41 | ]);
42 |
43 | $favicon = (new HttpDriver())->fetch('https://example.com');
44 |
45 | self::assertSame($expectedFaviconUrl, $favicon->getFaviconUrl());
46 | self::assertSame($expectedSize, $favicon->getIconSize());
47 | self::assertSame($expectedType, $favicon->getIconType());
48 | }
49 |
50 | /** @test */
51 | public function favicon_can_be_fetched_if_the_url_has_a_path_and_thelink_element_contains_a_relative_url(): void
52 | {
53 | Http::fake([
54 | 'https://example.com/blog' => Http::response(self::htmlOptionOne()),
55 | 'https://example.com/icon/is/here.ico' => Http::response('favicon contents here'),
56 | '*' => Http::response('should not hit here'),
57 | ]);
58 |
59 | $favicon = (new HttpDriver())->fetch('https://example.com/blog');
60 |
61 | self::assertSame('https://example.com/icon/is/here.ico', $favicon->getFaviconUrl());
62 | }
63 |
64 | /** @test */
65 | public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html(): void
66 | {
67 | $responseHtml = <<<'HTML'
68 |
69 |
70 |
71 | HTML;
72 |
73 | Http::fake([
74 | 'https://example.com' => Http::response($responseHtml),
75 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
76 | '*' => Http::response('should not hit here'),
77 | ]);
78 |
79 | $favicon = (new HttpDriver())->fetch('https://example.com');
80 |
81 | self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());
82 | }
83 |
84 | /** @test */
85 | public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_and_a_relative_url_is_passed(): void
86 | {
87 | $responseHtml = <<<'HTML'
88 |
89 |
90 |
91 | HTML;
92 |
93 | Http::fake([
94 | 'https://example.com/blog' => Http::response($responseHtml),
95 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
96 | '*' => Http::response('should not hit here'),
97 | ]);
98 |
99 | $favicon = (new HttpDriver())->fetch('https://example.com/blog');
100 |
101 | self::assertSame('https://example.com/favicon.ico', $favicon->getFaviconUrl());
102 | }
103 |
104 | /**
105 | * @test
106 | *
107 | * @testWith ["https"]
108 | * ["http"]
109 | */
110 | public function favicon_can_be_fetched_from_driver(string $protocol): void
111 | {
112 | Http::fake([
113 | 'https://example.com' => Http::response(''),
114 | 'http://example.com' => Http::response(''),
115 | '*' => Http::response('should not hit here'),
116 | ]);
117 |
118 | $favicon = (new HttpDriver())->fetch($protocol.'://example.com');
119 |
120 | self::assertSame($protocol.'://example.com/icon/favicon.ico', $favicon->getFaviconUrl());
121 | }
122 |
123 | /** @test */
124 | public function favicon_can_be_fetched_from_url_with_port(): void
125 | {
126 | Http::fake([
127 | 'http://example.com:8080' => Http::response(''),
128 | '*' => Http::response('should not hit here'),
129 | ]);
130 |
131 | $favicon = (new HttpDriver())->fetch('http://example.com:8080');
132 |
133 | self::assertSame('http://example.com:8080/icon/favicon.ico', $favicon->getFaviconUrl());
134 | }
135 |
136 | /** @test */
137 | public function favicon_can_be_fetched_from_url_with_query_parameters(): void
138 | {
139 | Http::fake([
140 | 'http://example.com?query=parameter' => Http::response(''),
141 | '*' => Http::response('should not hit here'),
142 | ]);
143 |
144 | $favicon = (new HttpDriver())->fetch('http://example.com?query=parameter');
145 |
146 | self::assertSame('http://example.com/icon/favicon.ico', $favicon->getFaviconUrl());
147 | }
148 |
149 | /** @test */
150 | public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
151 | {
152 | Cache::put(
153 | 'favicon-fetcher.example.com',
154 | [
155 | 'favicon_url' => 'url-goes-here',
156 | 'icon_size' => null,
157 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
158 | ],
159 | now()->addHour()
160 | );
161 |
162 | Http::fake([
163 | '*' => Http::response('should not hit here'),
164 | ]);
165 |
166 | $favicon = (new HttpDriver())->fetch('https://example.com');
167 |
168 | self::assertSame('url-goes-here', $favicon->getFaviconUrl());
169 | self::assertNull($favicon->getIconSize());
170 | self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicon->getIconType());
171 | }
172 |
173 | /** @test */
174 | public function favicon_can_be_fetched_from_the_cache_if_it_already_exists_in_the_old_string_format(): void
175 | {
176 | Cache::put(
177 | 'favicon-fetcher.example.com',
178 | 'url-goes-here',
179 | now()->addHour()
180 | );
181 |
182 | Http::fake([
183 | '*' => Http::response('should not hit here'),
184 | ]);
185 |
186 | $favicon = (new HttpDriver())->fetch('https://example.com');
187 |
188 | self::assertSame('url-goes-here', $favicon->getFaviconUrl());
189 | self::assertNull($favicon->getIconSize());
190 | self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicon->getIconType());
191 | }
192 |
193 | /** @test */
194 | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
195 | {
196 | Cache::put(
197 | 'favicon-fetcher.https://example.com',
198 | 'url-goes-here',
199 | now()->addHour()
200 | );
201 |
202 | Http::fake([
203 | 'https://example.com' => Http::response(''),
204 | '*' => Http::response('should not hit here'),
205 | ]);
206 |
207 | $favicon = (new HttpDriver())->useCache(false)->fetch('https://example.com');
208 |
209 | self::assertSame('https://example.com/icon/favicon.ico', $favicon->getFaviconUrl());
210 | }
211 |
212 | /** @test */
213 | public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
214 | {
215 | Http::fake([
216 | 'https://example.com/*' => Http::response('not found', 404),
217 | '*' => Http::response('should not hit here'),
218 | ]);
219 |
220 | $favicon = (new HttpDriver())->useCache(true)->fetch('https://example.com');
221 |
222 | self::assertNull($favicon);
223 | }
224 |
225 | /** @test */
226 | public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
227 | {
228 | Http::fake([
229 | 'https://example.com/*' => Http::response('not found', 404),
230 | '*' => Http::response('should not hit here'),
231 | ]);
232 |
233 | FetcherManager::extend('custom-driver', new CustomDriver());
234 |
235 | $favicon = (new HttpDriver())
236 | ->withFallback('custom-driver')
237 | ->useCache(true)
238 | ->fetch('https://example.com');
239 |
240 | self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
241 | }
242 |
243 | /** @test */
244 | public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
245 | {
246 | Http::fake([
247 | 'https://example.com/*' => Http::response('not found', 404),
248 | '*' => Http::response('should not hit here'),
249 | ]);
250 |
251 | $exception = null;
252 |
253 | try {
254 | (new HttpDriver())
255 | ->throw()
256 | ->useCache(true)
257 | ->fetch('https://example.com');
258 | } catch (\Exception $e) {
259 | $exception = $e;
260 | }
261 |
262 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
263 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
264 | }
265 |
266 | /** @test */
267 | public function default_value_can_be_returned_using_fetchOr_method(): void
268 | {
269 | Http::fake([
270 | 'https://example.com/*' => Http::response('not found', 404),
271 | '*' => Http::response('should not hit here'),
272 | ]);
273 |
274 | $favicon = (new HttpDriver())
275 | ->useCache(true)
276 | ->fetchOr('https://example.com', 'fallback-to-this');
277 |
278 | self::assertSame('fallback-to-this', $favicon);
279 | }
280 |
281 | /** @test */
282 | public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
283 | {
284 | Http::fake([
285 | 'https://example.com/*' => Http::response('not found', 404),
286 | '*' => Http::response('should not hit here'),
287 | ]);
288 |
289 | $favicon = (new HttpDriver())
290 | ->fetchOr('https://example.com', function () {
291 | return 'fallback-to-this';
292 | });
293 |
294 | self::assertSame('fallback-to-this', $favicon);
295 | }
296 |
297 | /** @test */
298 | public function exception_can_be_thrown_after_attempting_a_fallback(): void
299 | {
300 | Http::fake([
301 | 'https://example.com/*' => Http::response('not found', 404),
302 | '*' => Http::response('should not hit here'),
303 | ]);
304 |
305 | FetcherManager::extend('custom-driver', new NullDriver());
306 |
307 | $exception = null;
308 |
309 | try {
310 | (new HttpDriver())
311 | ->throw()
312 | ->withFallback('custom-driver')
313 | ->fetch('https://example.com');
314 | } catch (\Exception $e) {
315 | $exception = $e;
316 | }
317 |
318 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
319 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
320 |
321 | self::assertTrue(NullDriver::$flag);
322 | }
323 |
324 | /** @test */
325 | public function exception_is_thrown_if_the_url_is_invalid(): void
326 | {
327 | Http::fake([
328 | '*' => Http::response('should not hit here'),
329 | ]);
330 |
331 | $exception = null;
332 |
333 | try {
334 | (new HttpDriver())->fetch('example.com');
335 | } catch (\Exception $e) {
336 | $exception = $e;
337 | }
338 |
339 | self::assertInstanceOf(InvalidUrlException::class, $exception);
340 | self::assertSame('example.com is not a valid URL', $exception->getMessage());
341 | }
342 |
343 | /**
344 | * @test
345 | *
346 | * @dataProvider allFaviconLinksInHtmlProvider
347 | */
348 | public function all_icons_for_a_url_can_be_fetched(string $html, $expectedFaviconCollection): void
349 | {
350 | Http::fake([
351 | 'https://example.com' => Http::response($html),
352 | '*' => Http::response('should not hit here'),
353 | ]);
354 |
355 | $favicons = (new HttpDriver())->fetchAll('https://example.com');
356 |
357 | self::assertCount($expectedFaviconCollection->count(), $favicons);
358 |
359 | foreach ($favicons as $index => $favicon) {
360 | self::assertSame($expectedFaviconCollection[$index]->getFaviconUrl(), $favicon->getFaviconUrl());
361 | self::assertSame($expectedFaviconCollection[$index]->getIconType(), $favicon->getIconType());
362 | self::assertSame($expectedFaviconCollection[$index]->getIconSize(), $favicon->getIconSize());
363 | }
364 | }
365 |
366 | /** @test */
367 | public function favicon_can_be_fetched_from_guessed_url_if_it_cannot_be_found_in_response_html_when_trying_to_get_all_icons(): void
368 | {
369 | $responseHtml = <<<'HTML'
370 |
371 |
372 |
373 | HTML;
374 |
375 | Http::fake([
376 | 'https://example.com' => Http::response($responseHtml),
377 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
378 | '*' => Http::response('should not hit here'),
379 | ]);
380 |
381 | $favicons = (new HttpDriver())->fetchAll('https://example.com');
382 |
383 | self::assertCount(1, $favicons);
384 | self::assertSame($favicons->first()->getFaviconUrl(), 'https://example.com/favicon.ico');
385 | }
386 |
387 | /** @test */
388 | public function empty_favicon_collection_is_returned_if_the_url_cannot_be_reached(): void
389 | {
390 | Http::fake([
391 | 'https://example.com' => Http::response('not found', 404),
392 | 'https://example.com/favicon.ico' => Http::response('not found', 404),
393 | '*' => Http::response('should not hit here'),
394 | ]);
395 |
396 | $favicons = (new HttpDriver())->fetchAll('https://example.com');
397 |
398 | self::assertCount(0, $favicons);
399 | }
400 |
401 | /** @test */
402 | public function empty_favicon_collection_is_returned_if_no_icons_can_be_found_for_a_url(): void
403 | {
404 | $responseHtml = <<<'HTML'
405 |
406 |
407 |
408 | HTML;
409 |
410 | Http::fake([
411 | 'https://example.com' => Http::response($responseHtml),
412 | 'https://example.com/favicon.ico' => Http::response('not found', 404),
413 | '*' => Http::response('should not hit here'),
414 | ]);
415 |
416 | $favicons = (new HttpDriver())->fetchAll('https://example.com');
417 |
418 | self::assertCount(0, $favicons);
419 | }
420 |
421 | /** @test */
422 | public function error_is_thrown_if_trying_to_find_all_the_favicons_for_an_invalid_url(): void
423 | {
424 | Http::fake([
425 | '*' => Http::response('should not hit here'),
426 | ]);
427 |
428 | $exception = null;
429 |
430 | try {
431 | (new HttpDriver())->fetchAll('example.com');
432 | } catch (\Exception $e) {
433 | $exception = $e;
434 | }
435 |
436 | self::assertInstanceOf(InvalidUrlException::class, $exception);
437 | self::assertSame('example.com is not a valid URL', $exception->getMessage());
438 | }
439 |
440 | /** @test */
441 | public function error_is_thrown_if_no_icons_can_be_found_for_a_url_and_the_throw_on_not_found_flag_is_true(): void
442 | {
443 | $responseHtml = <<<'HTML'
444 |
445 |
446 |
447 | HTML;
448 |
449 | Http::fake([
450 | 'https://example.com' => Http::response($responseHtml),
451 | '*' => Http::response('should not hit here'),
452 | ]);
453 |
454 | $exception = null;
455 |
456 | try {
457 | (new HttpDriver())
458 | ->throw()
459 | ->fetchAll('https://example.com');
460 | } catch (\Exception $e) {
461 | $exception = $e;
462 | }
463 |
464 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
465 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
466 | }
467 |
468 | /** @test */
469 | public function all_favicon_for_a_url_can_be_fetched_from_the_cache_if_it_already_exists(): void
470 | {
471 | Cache::put(
472 | 'favicon-fetcher.example.com.collection',
473 | [
474 | [
475 | 'favicon_url' => 'url-goes-here',
476 | 'icon_size' => null,
477 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
478 | ],
479 | [
480 | 'favicon_url' => 'url-goes-here-1',
481 | 'icon_size' => 100,
482 | 'icon_type' => Favicon::TYPE_ICON,
483 | ],
484 | [
485 | 'favicon_url' => 'url-goes-here-1.com',
486 | 'icon_size' => 192,
487 | 'icon_type' => Favicon::TYPE_APPLE_TOUCH_ICON,
488 | ],
489 | ],
490 | now()->addHour(),
491 | );
492 |
493 | Http::fake([
494 | '*' => Http::response('should not hit here'),
495 | ]);
496 |
497 | $favicons = (new HttpDriver())->fetchAll('https://example.com');
498 |
499 | self::assertCount(3, $favicons);
500 |
501 | self::assertSame('url-goes-here', $favicons->first()->getFaviconUrl());
502 | self::assertSame('url-goes-here-1', $favicons->skip(1)->first()->getFaviconUrl());
503 | self::assertSame('url-goes-here-1.com', $favicons->skip(2)->first()->getFaviconUrl());
504 |
505 | self::assertNull($favicons->first()->getIconSize());
506 | self::assertSame(100, $favicons->skip(1)->first()->getIconSize());
507 | self::assertSame(192, $favicons->skip(2)->first()->getIconSize());
508 |
509 | self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $favicons->first()->getIconType());
510 | self::assertSame(Favicon::TYPE_ICON, $favicons->skip(1)->first()->getIconType());
511 | self::assertSame(Favicon::TYPE_APPLE_TOUCH_ICON, $favicons->skip(2)->first()->getIconType());
512 | }
513 |
514 | /** @test */
515 | public function all_favicons_for_a_url_are_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
516 | {
517 | Cache::put(
518 | 'favicon-fetcher.example.com.collection',
519 | [
520 | [
521 | 'favicon_url' => 'url-goes-here',
522 | 'icon_size' => null,
523 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
524 | ],
525 | [
526 | 'favicon_url' => 'url-goes-here-1',
527 | 'icon_size' => 100,
528 | 'icon_type' => Favicon::TYPE_ICON,
529 | ],
530 | [
531 | 'favicon_url' => 'url-goes-here-1.com',
532 | 'icon_size' => 192,
533 | 'icon_type' => Favicon::TYPE_APPLE_TOUCH_ICON,
534 | ],
535 | ],
536 | now()->addHour(),
537 | );
538 |
539 | Http::fake([
540 | 'https://example.com' => Http::response(''),
541 | '*' => Http::response('should not hit here'),
542 | ]);
543 |
544 | $favicons = (new HttpDriver())->useCache(false)->fetchAll('https://example.com');
545 |
546 | self::assertCount(1, $favicons);
547 |
548 | self::assertSame('https://example.com/icon/favicon.ico', $favicons->first()->getFaviconUrl());
549 | }
550 |
551 | /** @test */
552 | public function favicons_can_be_returned_using_the_fetchAllOr_method(): void
553 | {
554 | Http::fake([
555 | 'https://example.com' => Http::response(self::htmlOptionOne()),
556 | '*' => Http::response('should not hit here'),
557 | ]);
558 |
559 | $favicons = (new HttpDriver())->fetchAllOr('https://example.com', 'should not fallback to this');
560 |
561 | self::assertCount(1, $favicons);
562 |
563 | self::assertSame('https://example.com/icon/is/here.ico', $favicons->first()->getFaviconUrl());
564 | self::assertSame(Favicon::TYPE_ICON, $favicons->first()->getIconType());
565 | self::assertSame(null, $favicons->first()->getIconSize());
566 | }
567 |
568 | /** @test */
569 | public function default_value_can_be_returned_using_fetchAllOr_method(): void
570 | {
571 | Http::fake([
572 | 'https://example.com/*' => Http::response('not found', 404),
573 | '*' => Http::response('should not hit here'),
574 | ]);
575 |
576 | $favicon = (new HttpDriver())
577 | ->useCache(true)
578 | ->fetchAllOr('https://example.com', 'fallback-to-this');
579 |
580 | self::assertSame('fallback-to-this', $favicon);
581 | }
582 |
583 | /** @test */
584 | public function default_value_can_be_returned_using_fetchAllOr_method_with_a_closure(): void
585 | {
586 | Http::fake([
587 | 'https://example.com/*' => Http::response('not found', 404),
588 | '*' => Http::response('should not hit here'),
589 | ]);
590 |
591 | $favicon = (new HttpDriver())
592 | ->fetchAllOr('https://example.com', function () {
593 | return 'fallback-to-this';
594 | });
595 |
596 | self::assertSame('fallback-to-this', $favicon);
597 | }
598 |
599 | /** @test */
600 | public function can_set_the_user_agent_when_fetching()
601 | {
602 | Http::fake();
603 |
604 | $driver = new HttpDriver();
605 |
606 | // No user agent set.
607 | $driver->fetch('https://example.com');
608 |
609 | Http::assertSent(function (Request $request) {
610 | return $request->hasHeader('User-Agent', 'GuzzleHttp/7');
611 | });
612 |
613 | // Custom user agent.
614 | config()->set('favicon-fetcher.user_agent', 'test-user-agent');
615 |
616 | $driver->fetch('https://example.com');
617 |
618 | Http::assertSent(function (Request $request) {
619 | return $request->hasHeader('User-Agent', 'test-user-agent');
620 | });
621 | }
622 |
623 | /** @test */
624 | public function null_is_returned_if_using_fetch_and_the_link_has_no_href(): void
625 | {
626 | $responseHtml = <<<'HTML'
627 |
628 |
629 | Dummy title
630 |
631 |
632 |
633 |
634 | HTML;
635 |
636 | Http::preventStrayRequests();
637 |
638 | Http::fake([
639 | 'https://example.com' => Http::response($responseHtml),
640 | 'https://example.com/favicon.ico' => Http::response(status: 404),
641 | ]);
642 |
643 | $favicon = (new HttpDriver())->fetch('https://example.com');
644 |
645 | self::assertNull($favicon);
646 | }
647 |
648 | /** @test */
649 | public function null_is_returned_if_using_fetchAll_and_the_link_has_no_href(): void
650 | {
651 | $responseHtml = <<<'HTML'
652 |
653 |
654 | Dummy title
655 |
656 |
657 |
658 |
659 | HTML;
660 |
661 | Http::preventStrayRequests();
662 |
663 | Http::fake([
664 | 'https://example.com' => Http::response($responseHtml),
665 | 'https://example.com/favicon.ico' => Http::response(status: 404),
666 | ]);
667 |
668 | $favicons = (new HttpDriver())->fetchAll('https://example.com');
669 |
670 | self::assertCount(0, $favicons);
671 | }
672 |
673 | public static function allFaviconLinksInHtmlProvider(): array
674 | {
675 | return [
676 | [
677 | self::htmlOptionOne(),
678 | FaviconCollection::make([
679 | (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_ICON),
680 | ]),
681 | ],
682 | [
683 | self::htmlOptionTwo(),
684 | FaviconCollection::make([
685 | (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_ICON),
686 | ]),
687 | ],
688 | [
689 | self::htmlOptionThree(),
690 | FaviconCollection::make([
691 | (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
692 | ]),
693 | ],
694 | [
695 | self::htmlOptionFour(),
696 | FaviconCollection::make([
697 | (new Favicon('https://example.com', 'https://example.com/favicon/favicon-32x32.png'))->setIconSize(null)->setIconType(Favicon::TYPE_ICON),
698 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-57x57.png'))->setIconSize(57)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
699 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-60x60.png'))->setIconSize(60)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
700 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(72)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
701 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-72x72.png'))->setIconSize(76)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
702 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-76x76.png'))->setIconSize(114)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
703 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-120x120.png'))->setIconSize(120)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
704 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-144x144.png'))->setIconSize(144)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
705 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-152x152.png'))->setIconSize(152)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
706 | (new Favicon('https://example.com', 'https://example.com/favicon/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
707 | (new Favicon('https://example.com', 'https://example.com/favicon/android-icon-192x192.png'))->setIconSize(192)->setIconType(Favicon::TYPE_ICON),
708 | ]),
709 | ],
710 | [
711 | self::htmlOptionFive(),
712 | FaviconCollection::make([
713 | (new Favicon('https://example.com', 'https://example.com/icon/is/here.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
714 | ]),
715 | ],
716 | [
717 | self::htmlOptionSix(),
718 | FaviconCollection::make([
719 | (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
720 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
721 | ]),
722 | ],
723 | [
724 | self::htmlOptionSeven(),
725 | FaviconCollection::make([
726 | (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
727 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
728 | ]),
729 | ],
730 | [
731 | self::htmlOptionEight(),
732 | FaviconCollection::make([
733 | (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
734 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_ICON),
735 | ]),
736 | ],
737 | [
738 | self::htmlOptionNine(),
739 | FaviconCollection::make([
740 | (new Favicon('https://example.com', 'https://example.com/images/apple-icon-180x180.png'))->setIconSize(180)->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
741 | (new Favicon('https://example.com', 'https://example.com/images/favicon.ico'))->setIconType(Favicon::TYPE_ICON),
742 | ]),
743 | ],
744 | [
745 | self::htmlOptionTen(),
746 | FaviconCollection::make([
747 | (new Favicon('https://example.com', 'https://www.example.com/favicon123.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON),
748 | (new Favicon('https://example.com', 'https://www.example.com/favicon123.ico'))->setIconType(Favicon::TYPE_SHORTCUT_ICON),
749 | ]),
750 | ],
751 | [
752 | self::htmlOptionEleven(),
753 | FaviconCollection::make([
754 | (new Favicon('https://example.com', 'https://example.com/apple-icon-57x57.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(57),
755 | (new Favicon('https://example.com', 'https://example.com/apple-icon-60x60.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(60),
756 | (new Favicon('https://example.com', 'https://example.com/apple-icon-72x72.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(72),
757 | (new Favicon('https://example.com', 'https://example.com/apple-icon-76x76.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(76),
758 | (new Favicon('https://example.com', 'https://example.com/apple-icon-114x114.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(114),
759 | (new Favicon('https://example.com', 'https://example.com/apple-icon-120x120.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(120),
760 | (new Favicon('https://example.com', 'https://example.com/apple-icon-144x144.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(144),
761 | (new Favicon('https://example.com', 'https://example.com/apple-icon-152x152.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(152),
762 | (new Favicon('https://example.com', 'https://example.com/apple-icon-200x200.png'))->setIconType(Favicon::TYPE_APPLE_TOUCH_ICON)->setIconSize(200),
763 | (new Favicon('https://example.com', 'https://example.com/android-icon-192x192.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(192),
764 | (new Favicon('https://example.com', 'https://example.com/favicon-32x32.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(32),
765 | (new Favicon('https://example.com', 'https://example.com/favicon-96x96.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(96),
766 | ]),
767 | ],
768 | [
769 | self::htmlOptionThirteen(),
770 | FaviconCollection::make([
771 | (new Favicon('https://example.com', 'https://example.com/favicon-96x96.png'))->setIconType(Favicon::TYPE_ICON)->setIconSize(96),
772 | ]),
773 | ],
774 | ];
775 | }
776 |
777 | public static function faviconLinksInHtmlProvider(): array
778 | {
779 | return [
780 | [self::htmlOptionOne(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_ICON],
781 | [self::htmlOptionTwo(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_ICON],
782 | [self::htmlOptionThree(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_SHORTCUT_ICON],
783 | [self::htmlOptionFour(), 'https://example.com/favicon/favicon-32x32.png', null, Favicon::TYPE_ICON],
784 | [self::htmlOptionFive(), 'https://example.com/icon/is/here.ico', null, Favicon::TYPE_SHORTCUT_ICON],
785 | [self::htmlOptionSix(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_SHORTCUT_ICON],
786 | [self::htmlOptionSeven(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_SHORTCUT_ICON],
787 | [self::htmlOptionEight(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_ICON],
788 | [self::htmlOptionNine(), 'https://example.com/images/favicon.ico', null, Favicon::TYPE_ICON],
789 | [self::htmlOptionTen(), 'https://www.example.com/favicon123.ico', null, Favicon::TYPE_SHORTCUT_ICON],
790 | [self::htmlOptionEleven(), 'https://example.com/android-icon-192x192.png', 192, Favicon::TYPE_ICON],
791 | [self::htmlOptionTwelve(), 'https://example.com/android-icon-192x192.png', 192, Favicon::TYPE_ICON],
792 | [self::htmlOptionThirteen(), 'https://example.com/favicon-96x96.png', 96, Favicon::TYPE_ICON],
793 | ];
794 | }
795 |
796 | private static function htmlOptionOne(): string
797 | {
798 | return <<<'HTML'
799 |
800 |
801 |
802 | HTML;
803 | }
804 |
805 | private static function htmlOptionTwo(): string
806 | {
807 | return <<<'HTML'
808 |
809 |
810 |
811 | HTML;
812 | }
813 |
814 | private static function htmlOptionThree(): string
815 | {
816 | return <<<'HTML'
817 |
818 |
819 |
820 | HTML;
821 | }
822 |
823 | private static function htmlOptionFour(): string
824 | {
825 | return <<<'HTML'
826 |
827 |
828 |
829 | HTML;
830 | }
831 |
832 | private static function htmlOptionFive(): string
833 | {
834 | return <<<'HTML'
835 |
836 |
837 |
838 | HTML;
839 | }
840 |
841 | private static function htmlOptionSix(): string
842 | {
843 | return <<<'HTML'
844 | Title here
868 |
869 |
870 | HTML;
871 | }
872 |
873 | private static function htmlOptionEight(): string
874 | {
875 | return <<<'HTML'
876 | Title here
900 |
901 |
902 | HTML;
903 | }
904 |
905 | private static function htmlOptionTen(): string
906 | {
907 | return <<<'HTML'
908 |
909 | Test Title
910 |
911 |
912 |
913 |
914 |
915 |
916 | HTML;
917 | }
918 |
919 | private static function htmlOptionEleven(): string
920 | {
921 | return <<<'HTML'
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 | Dummy title
942 |
943 | HTML;
944 | }
945 |
946 | private static function htmlOptionTwelve(): string
947 | {
948 | return <<<'HTML'
949 |
950 |
951 |
952 |
953 |
954 |
955 |
956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 |
964 |
965 |
966 |
967 |
968 | Dummy title
969 |
970 | HTML;
971 | }
972 |
973 | private static function htmlOptionThirteen(): string
974 | {
975 | return <<<'HTML'
976 |
977 |
978 | Dummy title
979 |
980 |
981 |
982 |
983 |
984 | HTML;
985 | }
986 | }
987 |
--------------------------------------------------------------------------------
/tests/Feature/Drivers/UnavatarDriverTest.php:
--------------------------------------------------------------------------------
1 | Http::response('favicon contents here'),
33 | '*' => Http::response('should not hit here'),
34 | ]);
35 |
36 | $favicon = (new UnavatarDriver())->fetch($protocol.'://example.com');
37 |
38 | self::assertSame('https://unavatar.io/example.com?fallback=false', $favicon->getFaviconUrl());
39 | }
40 |
41 | /** @test */
42 | public function favicon_can_be_fetched_from_the_cache_if_it_already_exists(): void
43 | {
44 | Cache::put(
45 | 'favicon-fetcher.example.com',
46 | [
47 | 'favicon_url' => 'url-goes-here',
48 | 'icon_size' => null,
49 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
50 | ],
51 | now()->addHour()
52 | );
53 |
54 | Http::fake([
55 | '*' => Http::response('should not hit here'),
56 | ]);
57 |
58 | $favicon = (new UnavatarDriver())->fetch('https://example.com');
59 |
60 | self::assertSame('url-goes-here', $favicon->getFaviconUrl());
61 | }
62 |
63 | /** @test */
64 | public function favicon_is_not_fetched_from_the_cache_if_it_exists_but_the_use_cache_flag_is_false(): void
65 | {
66 | Cache::put(
67 | 'favicon-fetcher.https://example.com',
68 | 'url-goes-here',
69 | now()->addHour()
70 | );
71 |
72 | Http::fake([
73 | 'https://unavatar.io/example.com' => Http::response('favicon contents here'),
74 | '*' => Http::response('should not hit here'),
75 | ]);
76 |
77 | $favicon = (new UnavatarDriver())->useCache(false)->fetch('https://example.com');
78 |
79 | self::assertSame('https://unavatar.io/example.com?fallback=false', $favicon->getFaviconUrl());
80 | }
81 |
82 | /** @test */
83 | public function null_is_returned_if_the_driver_cannot_find_the_favicon(): void
84 | {
85 | Http::fake([
86 | 'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
87 | '*' => Http::response('should not hit here'),
88 | ]);
89 |
90 | $favicon = (new UnavatarDriver())->useCache(true)->fetch('https://example.com');
91 |
92 | self::assertNull($favicon);
93 | }
94 |
95 | /** @test */
96 | public function fallback_is_attempted_if_the_driver_cannot_find_the_favicon(): void
97 | {
98 | Http::fake([
99 | 'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
100 | '*' => Http::response('should not hit here'),
101 | ]);
102 |
103 | FetcherManager::extend('custom-driver', new CustomDriver());
104 |
105 | $favicon = (new UnavatarDriver())
106 | ->withFallback('custom-driver')
107 | ->useCache(true)
108 | ->fetch('https://example.com');
109 |
110 | self::assertSame('favicon-from-default', $favicon->getFaviconUrl());
111 | }
112 |
113 | /** @test */
114 | public function exception_is_thrown_if_the_driver_cannot_find_the_favicon_and_the_throw_on_not_found_flag_is_true(): void
115 | {
116 | Http::fake([
117 | 'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
118 | '*' => Http::response('should not hit here'),
119 | ]);
120 |
121 | $exception = null;
122 |
123 | try {
124 | (new UnavatarDriver())
125 | ->throw()
126 | ->useCache(true)
127 | ->fetch('https://example.com');
128 | } catch (\Exception $e) {
129 | $exception = $e;
130 | }
131 |
132 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
133 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
134 | }
135 |
136 | /** @test */
137 | public function default_value_can_be_returned_using_fetchOr_method(): void
138 | {
139 | Http::fake([
140 | 'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
141 | '*' => Http::response('should not hit here'),
142 | ]);
143 |
144 | $favicon = (new UnavatarDriver())
145 | ->useCache(true)
146 | ->fetchOr('https://example.com', 'fallback-to-this');
147 |
148 | self::assertSame('fallback-to-this', $favicon);
149 | }
150 |
151 | /** @test */
152 | public function default_value_can_be_returned_using_fetchOr_method_with_a_closure(): void
153 | {
154 | Http::fake([
155 | 'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
156 | '*' => Http::response('should not hit here'),
157 | ]);
158 |
159 | $favicon = (new UnavatarDriver())
160 | ->fetchOr('https://example.com', function () {
161 | return 'fallback-to-this';
162 | });
163 |
164 | self::assertSame('fallback-to-this', $favicon);
165 | }
166 |
167 | /** @test */
168 | public function exception_can_be_thrown_after_attempting_a_fallback(): void
169 | {
170 | Http::fake([
171 | 'https://unavatar.io/example.com?fallback=false' => Http::response('not found', 404),
172 | '*' => Http::response('should not hit here'),
173 | ]);
174 |
175 | FetcherManager::extend('custom-driver', new NullDriver());
176 |
177 | $exception = null;
178 |
179 | try {
180 | (new UnavatarDriver())
181 | ->throw()
182 | ->withFallback('custom-driver')
183 | ->fetch('https://example.com');
184 | } catch (\Exception $e) {
185 | $exception = $e;
186 | }
187 |
188 | self::assertInstanceOf(FaviconNotFoundException::class, $exception);
189 | self::assertSame('A favicon cannot be found for https://example.com', $exception->getMessage());
190 |
191 | self::assertTrue(NullDriver::$flag);
192 | }
193 |
194 | /** @test */
195 | public function exception_is_thrown_if_the_url_is_invalid(): void
196 | {
197 | Http::fake([
198 | '*' => Http::response('should not hit here'),
199 | ]);
200 |
201 | $exception = null;
202 |
203 | try {
204 | (new UnavatarDriver())->fetch('example.com');
205 | } catch (\Exception $e) {
206 | $exception = $e;
207 | }
208 |
209 | self::assertInstanceOf(InvalidUrlException::class, $exception);
210 | self::assertSame('example.com is not a valid URL', $exception->getMessage());
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/tests/Feature/FaviconTest.php:
--------------------------------------------------------------------------------
1 | getFaviconUrl());
32 | }
33 |
34 | /** @test */
35 | public function favicon_contents_can_be_returned(): void
36 | {
37 | Http::fake([
38 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
39 | '*' => Http::response('should not hit here'),
40 | ]);
41 |
42 | $favicon = new Favicon(
43 | url: 'https://example.com',
44 | faviconUrl: 'https://example.com/favicon.ico',
45 | );
46 |
47 | self::assertSame('favicon contents here', $favicon->content());
48 | }
49 |
50 | /** @test */
51 | public function url_can_be_returned(): void
52 | {
53 | $favicon = new Favicon(
54 | url: 'https://example.com',
55 | faviconUrl: 'https://example.com/favicon.ico',
56 | );
57 |
58 | self::assertSame('https://example.com', $favicon->getUrl());
59 | }
60 |
61 | /** @test */
62 | public function retrieved_from_cache_value_can_be_returned_if_the_favicon_was_retrieved_from_the_cache(): void
63 | {
64 | $favicon = new Favicon(
65 | url: 'https://example.com',
66 | faviconUrl: 'https://example.com/favicon.ico',
67 | retrievedFromCache: true,
68 | );
69 |
70 | self::assertTrue($favicon->retrievedFromCache());
71 | }
72 |
73 | /** @test */
74 | public function retrieved_from_cache_value_can_be_returned_if_the_favicon_was_not_retrieved_from_the_cache(): void
75 | {
76 | $favicon = new Favicon(
77 | url: 'https://example.com',
78 | faviconUrl: 'https://example.com/favicon.ico',
79 | );
80 |
81 | self::assertFalse($favicon->retrievedFromCache());
82 | }
83 |
84 | /** @test */
85 | public function favicon_can_be_cached_if_it_is_not_already_cached(): void
86 | {
87 | Carbon::setTestNow(now());
88 |
89 | $expectedTtl = now()->addMinute();
90 |
91 | Cache::shouldReceive('put')
92 | ->withArgs([
93 | 'favicon-fetcher.example.com',
94 | [
95 | 'favicon_url' => 'https://example.com/favicon.ico',
96 | 'icon_size' => null,
97 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
98 | ],
99 | Mockery::on(fn (CarbonInterface $ttl): bool => $ttl->eq($expectedTtl)),
100 | ])
101 | ->once();
102 |
103 | (new Favicon(
104 | url: 'https://example.com',
105 | faviconUrl: 'https://example.com/favicon.ico',
106 | ))->cache($expectedTtl);
107 | }
108 |
109 | /** @test */
110 | public function favicon_cannot_be_cached_if_it_is_already_cached(): void
111 | {
112 | Carbon::setTestNow(now());
113 |
114 | $expectedTtl = now()->addMinute();
115 |
116 | Cache::shouldReceive('put')->never();
117 |
118 | (new Favicon(
119 | url: 'https://example.com',
120 | faviconUrl: 'https://example.com/favicon.ico',
121 | retrievedFromCache: true,
122 | ))->cache($expectedTtl);
123 | }
124 |
125 | /** @test */
126 | public function favicon_can_be_cached_if_it_is_already_cached_and_the_force_flag_is_passed(): void
127 | {
128 | Carbon::setTestNow(now());
129 |
130 | $expectedTtl = now()->addMinute();
131 |
132 | Cache::shouldReceive('put')
133 | ->withArgs([
134 | 'favicon-fetcher.example.com',
135 | [
136 | 'favicon_url' => 'https://example.com/favicon.ico',
137 | 'icon_size' => null,
138 | 'icon_type' => Favicon::TYPE_ICON_UNKNOWN,
139 | ],
140 | Mockery::on(fn (CarbonInterface $ttl): bool => $ttl->eq($expectedTtl)),
141 | ])
142 | ->once();
143 |
144 | (new Favicon(
145 | 'https://example.com',
146 | 'https://example.com/favicon.ico',
147 | ))->cache(now()->addMinute(), true);
148 | }
149 |
150 | /** @test */
151 | public function favicon_contents_be_stored(): void
152 | {
153 | Storage::fake();
154 |
155 | Http::fake([
156 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
157 | '*' => Http::response('should not hit here'),
158 | ]);
159 |
160 | $favicon = new Favicon(
161 | url: 'https://example.com',
162 | faviconUrl: 'https://example.com/favicon.ico',
163 | );
164 |
165 | $path = $favicon->store('favicons');
166 |
167 | self::assertSame('favicon contents here', Storage::get($path));
168 | }
169 |
170 | /** @test */
171 | public function favicon_contents_be_stored_if_the_favicon_url_does_not_have_an_image_extension(): void
172 | {
173 | Storage::fake();
174 |
175 | Http::fake([
176 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
177 | 'https://example.com/favicon.com' => Http::response(body: 'favicon contents here', headers: ['content-type' => 'image/png']),
178 | '*' => Http::response('should not hit here'),
179 | ]);
180 |
181 | $favicon = new Favicon(
182 | url: 'https://example.com',
183 | faviconUrl: 'https://example.com/favicon.com',
184 | );
185 |
186 | $path = $favicon->store('favicons');
187 |
188 | self::assertSame('favicon contents here', Storage::get($path));
189 | self::assertTrue(Str::of($path)->endsWith('.png'));
190 | }
191 |
192 | /** @test */
193 | public function favicon_contents_can_be_stored_with_a_custom_file_name(): void
194 | {
195 | Storage::fake();
196 |
197 | Http::fake([
198 | 'https://example.com/favicon.ico' => Http::response('favicon contents here'),
199 | '*' => Http::response('should not hit here'),
200 | ]);
201 |
202 | (new Favicon(
203 | url: 'https://example.com',
204 | faviconUrl: 'https://example.com/favicon.ico',
205 | ))->storeAs('favicons', 'fetched');
206 |
207 | self::assertSame('favicon contents here', Storage::get('favicons/fetched.ico'));
208 | }
209 |
210 | public function icon_type_defaults_to_unknown_if_not_explicitly_set(): void
211 | {
212 | $iconType = (new Favicon(
213 | url: 'https://example.com',
214 | faviconUrl: 'https://example.com/favicon.ico',
215 | ))->getIconType();
216 |
217 | self::assertSame(Favicon::TYPE_ICON_UNKNOWN, $iconType);
218 | }
219 |
220 | /**
221 | * @test
222 | *
223 | * @dataProvider iconTypeProvider
224 | */
225 | public function icon_type_can_be_set_and_returned(string $expectedIconType): void
226 | {
227 | $iconType = (new Favicon(
228 | url: 'https://example.com',
229 | faviconUrl: 'https://example.com/favicon.ico',
230 | ))
231 | ->setIconType($expectedIconType)
232 | ->getIconType();
233 |
234 | self::assertSame($expectedIconType, $iconType);
235 | }
236 |
237 | /**
238 | * @test
239 | *
240 | * @dataProvider iconSizeProvider
241 | */
242 | public function icon_size_can_be_set_and_returned(?int $expectedIconSize): void
243 | {
244 | $iconSize = (new Favicon(
245 | url: 'https://example.com',
246 | faviconUrl: 'https://example.com/favicon.ico',
247 | ))
248 | ->setIconSize($expectedIconSize)
249 | ->getIconSize();
250 |
251 | self::assertSame($expectedIconSize, $iconSize);
252 | }
253 |
254 | /** @test */
255 | public function exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_type(): void
256 | {
257 | $this->expectException(InvalidIconTypeException::class);
258 | $this->expectExceptionMessage('The type [INVALID] is not a valid favicon type.');
259 |
260 | (new Favicon(
261 | url: 'https://example.com',
262 | faviconUrl: 'https://example.com/favicon.ico',
263 | ))->setIconType('INVALID');
264 | }
265 |
266 | /** @test */
267 | public function exception_is_thrown_when_trying_to_create_a_favicon_with_an_invalid_icon_size(): void
268 | {
269 | $this->expectException(InvalidIconSizeException::class);
270 | $this->expectExceptionMessage('The size [-1] is not a valid favicon size.');
271 |
272 | (new Favicon(
273 | url: 'https://example.com',
274 | faviconUrl: 'https://example.com/favicon.ico',
275 | ))->setIconSize(-1);
276 | }
277 |
278 | public static function iconTypeProvider(): array
279 | {
280 | return [
281 | [Favicon::TYPE_ICON],
282 | [Favicon::TYPE_SHORTCUT_ICON],
283 | [Favicon::TYPE_APPLE_TOUCH_ICON],
284 | [Favicon::TYPE_ICON_UNKNOWN],
285 | ];
286 | }
287 |
288 | public static function iconSizeProvider(): array
289 | {
290 | return [
291 | [null],
292 | [16],
293 | [190],
294 | ];
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/tests/Feature/FetcherManagerTest.php:
--------------------------------------------------------------------------------
1 | 'http']);
26 |
27 | self::assertInstanceOf(HttpDriver::class, FetcherManager::driver());
28 | }
29 |
30 | /** @test */
31 | public function http_driver_can_be_returned(): void
32 | {
33 | self::assertInstanceOf(HttpDriver::class, FetcherManager::driver('http'));
34 | }
35 |
36 | /** @test */
37 | public function google_shared_stuff_driver_can_be_returned(): void
38 | {
39 | self::assertInstanceOf(GoogleSharedStuffDriver::class, FetcherManager::driver('google-shared-stuff'));
40 | }
41 |
42 | /** @test */
43 | public function favicon_kit_driver_can_be_returned(): void
44 | {
45 | self::assertInstanceOf(FaviconKitDriver::class, FetcherManager::driver('favicon-kit'));
46 | }
47 |
48 | /** @test */
49 | public function favicon_grabber_driver_can_be_returned(): void
50 | {
51 | self::assertInstanceOf(FaviconGrabberDriver::class, FetcherManager::driver('favicon-grabber'));
52 | }
53 |
54 | /** @test */
55 | public function custom_driver_can_be_returned(): void
56 | {
57 | FetcherManager::extend('custom-driver', new CustomDriver());
58 | self::assertInstanceOf(CustomDriver::class, FetcherManager::driver('custom-driver'));
59 | }
60 |
61 | /** @test */
62 | public function exception_is_thrown_if_the_driver_is_invalid(): void
63 | {
64 | $this->expectException(FaviconFetcherException::class);
65 | $this->expectExceptionMessage('invalid is not a valid driver');
66 |
67 | FetcherManager::driver('invalid');
68 | }
69 |
70 | /** @test */
71 | public function method_calls_to_the_manager_are_forwarded_to_the_driver(): void
72 | {
73 | $mock = tap(
74 | Mockery::mock(CustomDriver::class),
75 | function (Mockery\MockInterface $mock): void {
76 | $mock->shouldReceive('fetch')
77 | ->once()
78 | ->withArgs(['https://example.com']);
79 | }
80 | );
81 |
82 | FetcherManager::extend('custom-driver', $mock);
83 |
84 | config(['favicon-fetcher.default' => 'custom-driver']);
85 |
86 | (new FetcherManager())->fetch('https://example.com');
87 | }
88 |
89 | /** @test */
90 | public function method_calls_to_the_manager_are_forwarded_to_the_driver_using_the_facade(): void
91 | {
92 | $mock = tap(
93 | Mockery::mock(CustomDriver::class),
94 | function (Mockery\MockInterface $mock): void {
95 | $mock->shouldReceive('fetch')
96 | ->once()
97 | ->withArgs(['https://example.com']);
98 | }
99 | );
100 |
101 | FetcherManager::extend('custom-driver', $mock);
102 |
103 | config(['favicon-fetcher.default' => 'custom-driver']);
104 |
105 | Favicon::fetch('https://example.com');
106 | }
107 |
108 | /** @test */
109 | public function driver_can_be_returned_using_the_facade(): void
110 | {
111 | self::assertInstanceOf(FaviconKitDriver::class, Favicon::driver('favicon-kit'));
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/tests/Feature/TestCase.php:
--------------------------------------------------------------------------------
1 |