Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
17 / 18
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
App
94.44% covered (success)
94.44%
17 / 18
66.67% covered (warning)
66.67%
2 / 3
13.03
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
 url
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 proxies
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
7.03
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Config;
6
7use Cosray\Exception\RuntimeException;
8use Cosray\Util\IpRange;
9use DateTimeZone;
10
11final class App
12{
13    private ?DateTimeZone $timezoneCache = null;
14
15    public function __construct(
16        private readonly \Cosray\Config $config,
17    ) {}
18
19    public string $name {
20        get => $this->config->get('app.name');
21    }
22
23    /**
24     * The site's name as people read it, in mails for example, unlike
25     * `app.name`, which names the session cookies.
26     */
27    public string $title {
28        get {
29            $title = $this->config->get('app.title');
30
31            if (is_string($title) && trim($title) !== '') {
32                return trim($title);
33            }
34
35            return parse_url((string) $this->url, PHP_URL_HOST) ?: $this->name;
36        }
37    }
38
39    public bool $debug {
40        get => $this->config->get('app.debug');
41    }
42
43    public string $env {
44        get => $this->config->get('app.env');
45    }
46
47    public string $urlPrefix {
48        get => $this->config->get('app.url_prefix');
49    }
50
51    /**
52     * The absolute URL the application is served under, like
53     * `https://example.org`, for links that leave the request behind, like
54     * those in mails. Never taken from the request's host header.
55     */
56    public ?string $url {
57        get => self::url($this->config->get('app.url'));
58    }
59
60    /**
61     * Addresses and CIDR ranges of reverse proxies whose `X-Forwarded-For`
62     * names the client.
63     *
64     * @var list<non-empty-string>
65     */
66    public array $trustedProxies {
67        get => self::proxies($this->config->get('app.trusted_proxies'));
68    }
69
70    public DateTimeZone $timezone {
71        get => $this->timezoneCache ??= new DateTimeZone($this->config->get('app.timezone'));
72    }
73
74    public ?string $secret {
75        get => $this->config->get('app.secret');
76    }
77
78    private static function url(mixed $url): ?string
79    {
80        if ($url === null || $url === '') {
81            return null;
82        }
83
84        if (!is_string($url) || preg_match('#^https?://[^/\s]+#i', $url) !== 1) {
85            throw new RuntimeException('`app.url` takes an absolute http or https URL, like https://example.org.');
86        }
87
88        return rtrim($url, '/');
89    }
90
91    /** @return list<non-empty-string> */
92    private static function proxies(mixed $proxies): array
93    {
94        $proxies = is_string($proxies) ? explode(',', $proxies) : $proxies;
95
96        if (!is_array($proxies)) {
97            throw new RuntimeException('`app.trusted_proxies` takes a list of IP addresses and CIDR ranges.');
98        }
99
100        $valid = [];
101
102        foreach ($proxies as $proxy) {
103            $proxy = is_string($proxy) ? trim($proxy) : '';
104
105            if ($proxy === '') {
106                continue;
107            }
108
109            if (!IpRange::valid($proxy)) {
110                throw new RuntimeException("`app.trusted_proxies` has {$proxy}, which is no IP address or CIDR range.");
111            }
112
113            $valid[] = $proxy;
114        }
115
116        return $valid;
117    }
118}