Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (warning)
80.00%
16 / 20
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceLoader
80.00% covered (warning)
80.00%
16 / 20
33.33% covered (danger)
33.33%
2 / 6
7.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetch
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 cache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 ssrfGuard
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 defaultOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fetcher
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\ResourceLoader;
6
7use Phpdftk\ResourceLoader\Cache\CacheInterface;
8use Phpdftk\ResourceLoader\Cache\NullCache;
9
10/**
11 * URL → bytes resolver with SSRF guard, caching, and bounded
12 * retry / redirect handling.
13 *
14 * Wiring stack:
15 *
16 *   ResourceLoader  →  cache lookup
17 *                  →  HttpFetcher
18 *                       →  SsrfGuard (pre-flight + per redirect hop)
19 *                       →  TransportInterface (curl by default)
20 *                       →  MimeSniffer
21 *                  →  cache store
22 */
23final class ResourceLoader
24{
25    private readonly HttpFetcher $fetcher;
26
27    public function __construct(
28        private readonly CacheInterface $cache = new NullCache(),
29        private readonly SsrfGuard $ssrfGuard = new SsrfGuard(),
30        private readonly FetchOptions $defaultOptions = new FetchOptions(),
31        ?HttpFetcher $fetcher = null,
32    ) {
33        $this->fetcher = $fetcher ?? new HttpFetcher(ssrfGuard: $this->ssrfGuard);
34    }
35
36    /**
37     * Fetch the resource at `$url`. Throws
38     * {@see Exception\SsrfBlockedException} when the URL violates
39     * the SSRF policy, {@see Exception\FetchFailedException} for
40     * everything else.
41     *
42     * Cache lookup is keyed by the requested URL (not the final
43     * post-redirect URL) — same URL means same result for the
44     * caller, regardless of how the server chose to redirect.
45     */
46    public function fetch(string $url, ?FetchOptions $options = null): FetchResult
47    {
48        $resolvedOptions = $options ?? $this->defaultOptions;
49
50        $this->ssrfGuard->assertSafe($url);
51
52        $cached = $this->cache->get($url);
53        if ($cached !== null) {
54            return new FetchResult(
55                bytes: $cached->bytes,
56                mimeType: $cached->mimeType,
57                originalUrl: $cached->originalUrl,
58                finalUrl: $cached->finalUrl,
59                cacheHit: true,
60                statusCode: $cached->statusCode,
61            );
62        }
63
64        $result = $this->fetcher->fetch($url, $resolvedOptions);
65        $this->cache->set($url, $result);
66        return $result;
67    }
68
69    public function cache(): CacheInterface
70    {
71        return $this->cache;
72    }
73
74    public function ssrfGuard(): SsrfGuard
75    {
76        return $this->ssrfGuard;
77    }
78
79    public function defaultOptions(): FetchOptions
80    {
81        return $this->defaultOptions;
82    }
83
84    public function fetcher(): HttpFetcher
85    {
86        return $this->fetcher;
87    }
88}