diff --git a/config/autoload/delete_short_urls.global.php b/config/autoload/delete_short_urls.global.php
index a3964e71..3d562f78 100644
--- a/config/autoload/delete_short_urls.global.php
+++ b/config/autoload/delete_short_urls.global.php
@@ -4,10 +4,10 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink;
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return (static function (): array {
-    $threshold = env('DELETE_SHORT_URL_THRESHOLD');
+    $threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD()->loadFromEnv();
 
     return [
 
diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php
index c83db2a8..d98d37dc 100644
--- a/config/autoload/entity-manager.global.php
+++ b/config/autoload/entity-manager.global.php
@@ -3,12 +3,12 @@
 declare(strict_types=1);
 
 use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 use function Functional\contains;
-use function Shlinkio\Shlink\Config\env;
 
 return (static function (): array {
-    $driver = env('DB_DRIVER');
+    $driver = EnvVars::DB_DRIVER()->loadFromEnv();
     $isMysqlCompatible = contains(['maria', 'mysql'], $driver);
 
     $resolveDriver = static fn () => match ($driver) {
@@ -35,12 +35,12 @@ return (static function (): array {
         ],
         default => [
             'driver' => $resolveDriver(),
-            'dbname' => env('DB_NAME', 'shlink'),
-            'user' => env('DB_USER'),
-            'password' => env('DB_PASSWORD'),
-            'host' => env('DB_HOST', env('DB_UNIX_SOCKET')),
-            'port' => env('DB_PORT', $resolveDefaultPort()),
-            'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null,
+            'dbname' => EnvVars::DB_NAME()->loadFromEnv('shlink'),
+            'user' => EnvVars::DB_USER()->loadFromEnv(),
+            'password' => EnvVars::DB_PASSWORD()->loadFromEnv(),
+            'host' => EnvVars::DB_HOST()->loadFromEnv(EnvVars::DB_UNIX_SOCKET()->loadFromEnv()),
+            'port' => EnvVars::DB_PORT()->loadFromEnv($resolveDefaultPort()),
+            'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET()->loadFromEnv() : null,
             'charset' => $resolveCharset(),
         ],
     };
diff --git a/config/autoload/geolite2.global.php b/config/autoload/geolite2.global.php
index fd11e52a..cf1f57fc 100644
--- a/config/autoload/geolite2.global.php
+++ b/config/autoload/geolite2.global.php
@@ -2,14 +2,14 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return [
 
     'geolite2' => [
         'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
         'temp_dir' => __DIR__ . '/../../data',
-        'license_key' => env('GEOLITE_LICENSE_KEY'),
+        'license_key' => EnvVars::GEOLITE_LICENSE_KEY()->loadFromEnv(),
     ],
 
 ];
diff --git a/config/autoload/locks.global.php b/config/autoload/locks.global.php
index 16fdbbca..8b018d1e 100644
--- a/config/autoload/locks.global.php
+++ b/config/autoload/locks.global.php
@@ -5,10 +5,9 @@ declare(strict_types=1);
 use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
 use Predis\ClientInterface as PredisClient;
 use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 use Symfony\Component\Lock;
 
-use function Shlinkio\Shlink\Config\env;
-
 use const Shlinkio\Shlink\LOCAL_LOCK_FACTORY;
 
 return [
@@ -25,7 +24,7 @@ return [
             LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
         ],
         'aliases' => [
-            'lock_store' => env('REDIS_SERVERS') === null ? 'local_lock_store' : 'redis_lock_store',
+            'lock_store' => EnvVars::REDIS_SERVERS()->existsInEnv() ? 'local_lock_store' : 'redis_lock_store',
 
             'redis_lock_store' => Lock\Store\RedisStore::class,
             'local_lock_store' => Lock\Store\FlockStore::class,
diff --git a/config/autoload/mercure.global.php b/config/autoload/mercure.global.php
index 7eb356ab..ba261369 100644
--- a/config/autoload/mercure.global.php
+++ b/config/autoload/mercure.global.php
@@ -4,20 +4,19 @@ declare(strict_types=1);
 
 use Laminas\ServiceManager\Proxy\LazyServiceFactory;
 use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 use Symfony\Component\Mercure\Hub;
 use Symfony\Component\Mercure\HubInterface;
 
-use function Shlinkio\Shlink\Config\env;
-
 return (static function (): array {
-    $publicUrl = env('MERCURE_PUBLIC_HUB_URL');
+    $publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL()->loadFromEnv();
 
     return [
 
         'mercure' => [
             'public_hub_url' => $publicUrl,
-            'internal_hub_url' => env('MERCURE_INTERNAL_HUB_URL', $publicUrl),
-            'jwt_secret' => env('MERCURE_JWT_SECRET'),
+            'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL()->loadFromEnv($publicUrl),
+            'jwt_secret' => EnvVars::MERCURE_JWT_SECRET()->loadFromEnv(),
             'jwt_issuer' => 'Shlink',
         ],
 
diff --git a/config/autoload/qr-codes.global.php b/config/autoload/qr-codes.global.php
index 7940ad18..d72198af 100644
--- a/config/autoload/qr-codes.global.php
+++ b/config/autoload/qr-codes.global.php
@@ -2,7 +2,7 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
 use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
@@ -13,11 +13,15 @@ use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
 return [
 
     'qr_codes' => [
-        'size' => (int) env('DEFAULT_QR_CODE_SIZE', DEFAULT_QR_CODE_SIZE),
-        'margin' => (int) env('DEFAULT_QR_CODE_MARGIN', DEFAULT_QR_CODE_MARGIN),
-        'format' => env('DEFAULT_QR_CODE_FORMAT', DEFAULT_QR_CODE_FORMAT),
-        'error_correction' => env('DEFAULT_QR_CODE_ERROR_CORRECTION', DEFAULT_QR_CODE_ERROR_CORRECTION),
-        'round_block_size' => (bool) env('DEFAULT_QR_CODE_ROUND_BLOCK_SIZE', DEFAULT_QR_CODE_ROUND_BLOCK_SIZE),
+        'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE()->loadFromEnv(DEFAULT_QR_CODE_SIZE),
+        'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN()->loadFromEnv(DEFAULT_QR_CODE_MARGIN),
+        'format' => EnvVars::DEFAULT_QR_CODE_FORMAT()->loadFromEnv(DEFAULT_QR_CODE_FORMAT),
+        'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION()->loadFromEnv(
+            DEFAULT_QR_CODE_ERROR_CORRECTION,
+        ),
+        'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE()->loadFromEnv(
+            DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
+        ),
     ],
 
 ];
diff --git a/config/autoload/rabbit.global.php b/config/autoload/rabbit.global.php
index adf304c8..faa5f569 100644
--- a/config/autoload/rabbit.global.php
+++ b/config/autoload/rabbit.global.php
@@ -5,18 +5,17 @@ declare(strict_types=1);
 use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
 use Laminas\ServiceManager\Proxy\LazyServiceFactory;
 use PhpAmqpLib\Connection\AMQPStreamConnection;
-
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return [
 
     'rabbitmq' => [
-        'enabled' => (bool) env('RABBITMQ_ENABLED', false),
-        'host' => env('RABBITMQ_HOST'),
-        'port' => (int) env('RABBITMQ_PORT', '5672'),
-        'user' => env('RABBITMQ_USER'),
-        'password' => env('RABBITMQ_PASSWORD'),
-        'vhost' => env('RABBITMQ_VHOST', '/'),
+        'enabled' => (bool) EnvVars::RABBITMQ_ENABLED()->loadFromEnv(false),
+        'host' => EnvVars::RABBITMQ_HOST()->loadFromEnv(),
+        'port' => (int) EnvVars::RABBITMQ_PORT()->loadFromEnv('5672'),
+        'user' => EnvVars::RABBITMQ_USER()->loadFromEnv(),
+        'password' => EnvVars::RABBITMQ_PASSWORD()->loadFromEnv(),
+        'vhost' => EnvVars::RABBITMQ_VHOST()->loadFromEnv('/'),
     ],
 
     'dependencies' => [
diff --git a/config/autoload/redirects.global.php b/config/autoload/redirects.global.php
index 20cde8eb..08439b2a 100644
--- a/config/autoload/redirects.global.php
+++ b/config/autoload/redirects.global.php
@@ -2,7 +2,7 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME;
 use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
@@ -10,14 +10,16 @@ use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
 return [
 
     'not_found_redirects' => [
-        'invalid_short_url' => env('DEFAULT_INVALID_SHORT_URL_REDIRECT'),
-        'regular_404' => env('DEFAULT_REGULAR_404_REDIRECT'),
-        'base_url' => env('DEFAULT_BASE_URL_REDIRECT'),
+        'invalid_short_url' => EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT()->loadFromEnv(),
+        'regular_404' => EnvVars::DEFAULT_REGULAR_404_REDIRECT()->loadFromEnv(),
+        'base_url' => EnvVars::DEFAULT_BASE_URL_REDIRECT()->loadFromEnv(),
     ],
 
     'redirects' => [
-        'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
-        'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
+        'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE()->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE),
+        'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME()->loadFromEnv(
+            DEFAULT_REDIRECT_CACHE_LIFETIME,
+        ),
     ],
 
 ];
diff --git a/config/autoload/redis.global.php b/config/autoload/redis.global.php
index 7af209d6..6bb1961e 100644
--- a/config/autoload/redis.global.php
+++ b/config/autoload/redis.global.php
@@ -2,10 +2,10 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return (static function (): array {
-    $redisServers = env('REDIS_SERVERS');
+    $redisServers = EnvVars::REDIS_SERVERS()->loadFromEnv();
 
     return match ($redisServers) {
         null => [],
@@ -14,7 +14,7 @@ return (static function (): array {
                 'default_lifetime' => 86400, // 24h
                 'redis' => [
                     'servers' => $redisServers,
-                    'sentinel_service' => env('REDIS_SENTINEL_SERVICE'),
+                    'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE()->loadFromEnv(),
                 ],
             ],
         ],
diff --git a/config/autoload/router.global.php b/config/autoload/router.global.php
index 55397e27..fd1f9525 100644
--- a/config/autoload/router.global.php
+++ b/config/autoload/router.global.php
@@ -3,13 +3,12 @@
 declare(strict_types=1);
 
 use Mezzio\Router\FastRouteRouter;
-
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return [
 
     'router' => [
-        'base_path' => env('BASE_PATH', ''),
+        'base_path' => EnvVars::BASE_PATH()->loadFromEnv(''),
 
         'fastroute' => [
             FastRouteRouter::CONFIG_CACHE_ENABLED => true,
diff --git a/config/autoload/swoole.global.php b/config/autoload/swoole.global.php
index d5a6fd55..9d2c423f 100644
--- a/config/autoload/swoole.global.php
+++ b/config/autoload/swoole.global.php
@@ -2,12 +2,12 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 use const Shlinkio\Shlink\MIN_TASK_WORKERS;
 
 return (static function () {
-    $taskWorkers = (int) env('TASK_WORKER_NUM', 16);
+    $taskWorkers = (int) EnvVars::TASK_WORKER_NUM()->loadFromEnv(16);
 
     return [
 
@@ -17,11 +17,11 @@ return (static function () {
 
             'swoole-http-server' => [
                 'host' => '0.0.0.0',
-                'port' => (int) env('PORT', 8080),
+                'port' => (int) EnvVars::PORT()->loadFromEnv(8080),
                 'process-name' => 'shlink',
 
                 'options' => [
-                    'worker_num' => (int) env('WEB_WORKER_NUM', 16),
+                    'worker_num' => (int) EnvVars::WEB_WORKER_NUM()->loadFromEnv(16),
                     'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
                 ],
             ],
diff --git a/config/autoload/tracking.global.php b/config/autoload/tracking.global.php
index 2dc23890..b2596830 100644
--- a/config/autoload/tracking.global.php
+++ b/config/autoload/tracking.global.php
@@ -2,35 +2,35 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return [
 
     'tracking' => [
         // Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
         // This applies only if IP address tracking is enabled
-        'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true),
+        'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR()->loadFromEnv(true),
 
         // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
-        'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true),
+        'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS()->loadFromEnv(true),
 
         // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
-        'disable_track_param' => env('DISABLE_TRACK_PARAM'),
+        'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM()->loadFromEnv(),
 
         // If true, visits will not be tracked at all
-        'disable_tracking' => (bool) env('DISABLE_TRACKING', false),
+        'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING()->loadFromEnv(false),
 
         // If true, visits will be tracked, but neither the IP address, nor the location will be resolved
-        'disable_ip_tracking' => (bool) env('DISABLE_IP_TRACKING', false),
+        'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING()->loadFromEnv(false),
 
         // If true, the referrer will not be tracked
-        'disable_referrer_tracking' => (bool) env('DISABLE_REFERRER_TRACKING', false),
+        'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING()->loadFromEnv(false),
 
         // If true, the user agent will not be tracked
-        'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false),
+        'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING()->loadFromEnv(false),
 
         // A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
-        'disable_tracking_from' => env('DISABLE_TRACKING_FROM'),
+        'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM()->loadFromEnv(),
     ],
 
 ];
diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php
index d5b4bfe5..25de914a 100644
--- a/config/autoload/url-shortener.global.php
+++ b/config/autoload/url-shortener.global.php
@@ -2,14 +2,14 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
 use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
 
 return (static function (): array {
     $shortCodesLength = max(
-        (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH),
+        (int) EnvVars::DEFAULT_SHORT_CODES_LENGTH()->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
         MIN_SHORT_CODES_LENGTH,
     );
 
@@ -17,12 +17,12 @@ return (static function (): array {
 
         'url_shortener' => [
             'domain' => [
-                'schema' => ((bool) env('IS_HTTPS_ENABLED', true)) ? 'https' : 'http',
-                'hostname' => env('DEFAULT_DOMAIN', ''),
+                'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED()->loadFromEnv(true)) ? 'https' : 'http',
+                'hostname' => EnvVars::DEFAULT_DOMAIN()->loadFromEnv(''),
             ],
             'default_short_codes_length' => $shortCodesLength,
-            'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
-            'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false),
+            'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES()->loadFromEnv(false),
+            'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH()->loadFromEnv(false),
         ],
 
     ];
diff --git a/config/autoload/webhooks.global.php b/config/autoload/webhooks.global.php
index 6bbbfbda..8e768e39 100644
--- a/config/autoload/webhooks.global.php
+++ b/config/autoload/webhooks.global.php
@@ -2,16 +2,17 @@
 
 declare(strict_types=1);
 
-use function Shlinkio\Shlink\Config\env;
+use Shlinkio\Shlink\Core\Config\EnvVars;
 
 return (static function (): array {
-    $webhooks = env('VISITS_WEBHOOKS');
+    $webhooks = EnvVars::VISITS_WEBHOOKS()->loadFromEnv();
 
     return [
 
         'visits_webhooks' => [
             'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
-            'notify_orphan_visits_to_webhooks' => (bool) env('NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS', false),
+            'notify_orphan_visits_to_webhooks' =>
+                (bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()->loadFromEnv(false),
         ],
 
     ];
diff --git a/config/config.php b/config/config.php
index 0adb0208..3dad2105 100644
--- a/config/config.php
+++ b/config/config.php
@@ -21,7 +21,7 @@ $isTestEnv = env('APP_ENV') === 'test';
 
 return (new ConfigAggregator\ConfigAggregator([
     ! $isTestEnv
-        ? new EnvVarLoaderProvider('config/params/generated_config.php')
+        ? new EnvVarLoaderProvider('config/params/generated_config.php', Core\Config\EnvVars::cases())
         : new ConfigAggregator\ArrayProvider([]),
     Mezzio\ConfigProvider::class,
     Mezzio\Router\ConfigProvider::class,
diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php
new file mode 100644
index 00000000..e3771e4c
--- /dev/null
+++ b/module/Core/src/Config/EnvVars.php
@@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shlinkio\Shlink\Core\Config;
+
+use ReflectionClass;
+use ReflectionClassConstant;
+use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
+
+use function array_values;
+use function Functional\contains;
+use function Shlinkio\Shlink\Config\env;
+
+// TODO Convert to enum
+
+/**
+ * @method static EnvVars DELETE_SHORT_URL_THRESHOLD()
+ * @method static EnvVars DB_DRIVER()
+ * @method static EnvVars DB_NAME()
+ * @method static EnvVars DB_USER()
+ * @method static EnvVars DB_PASSWORD()
+ * @method static EnvVars DB_HOST()
+ * @method static EnvVars DB_UNIX_SOCKET()
+ * @method static EnvVars DB_PORT()
+ * @method static EnvVars GEOLITE_LICENSE_KEY()
+ * @method static EnvVars REDIS_SERVERS()
+ * @method static EnvVars REDIS_SENTINEL_SERVICE()
+ * @method static EnvVars MERCURE_PUBLIC_HUB_URL()
+ * @method static EnvVars MERCURE_INTERNAL_HUB_URL()
+ * @method static EnvVars MERCURE_JWT_SECRET()
+ * @method static EnvVars DEFAULT_QR_CODE_SIZE()
+ * @method static EnvVars DEFAULT_QR_CODE_MARGIN()
+ * @method static EnvVars DEFAULT_QR_CODE_FORMAT()
+ * @method static EnvVars DEFAULT_QR_CODE_ERROR_CORRECTION()
+ * @method static EnvVars DEFAULT_QR_CODE_ROUND_BLOCK_SIZE()
+ * @method static EnvVars RABBITMQ_ENABLED()
+ * @method static EnvVars RABBITMQ_HOST()
+ * @method static EnvVars RABBITMQ_PORT()
+ * @method static EnvVars RABBITMQ_USER()
+ * @method static EnvVars RABBITMQ_PASSWORD()
+ * @method static EnvVars RABBITMQ_VHOST()
+ * @method static EnvVars DEFAULT_INVALID_SHORT_URL_REDIRECT()
+ * @method static EnvVars DEFAULT_REGULAR_404_REDIRECT()
+ * @method static EnvVars DEFAULT_BASE_URL_REDIRECT()
+ * @method static EnvVars REDIRECT_STATUS_CODE()
+ * @method static EnvVars REDIRECT_CACHE_LIFETIME()
+ * @method static EnvVars BASE_PATH()
+ * @method static EnvVars PORT()
+ * @method static EnvVars TASK_WORKER_NUM()
+ * @method static EnvVars WEB_WORKER_NUM()
+ * @method static EnvVars ANONYMIZE_REMOTE_ADDR()
+ * @method static EnvVars TRACK_ORPHAN_VISITS()
+ * @method static EnvVars DISABLE_TRACK_PARAM()
+ * @method static EnvVars DISABLE_TRACKING()
+ * @method static EnvVars DISABLE_IP_TRACKING()
+ * @method static EnvVars DISABLE_REFERRER_TRACKING()
+ * @method static EnvVars DISABLE_UA_TRACKING()
+ * @method static EnvVars DISABLE_TRACKING_FROM()
+ * @method static EnvVars DEFAULT_SHORT_CODES_LENGTH()
+ * @method static EnvVars IS_HTTPS_ENABLED()
+ * @method static EnvVars DEFAULT_DOMAIN()
+ * @method static EnvVars AUTO_RESOLVE_TITLES()
+ * @method static EnvVars REDIRECT_APPEND_EXTRA_PATH()
+ * @method static EnvVars VISITS_WEBHOOKS()
+ * @method static EnvVars NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()
+ */
+final class EnvVars
+{
+    public const DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD';
+    public const DB_DRIVER = 'DB_DRIVER';
+    public const DB_NAME = 'DB_NAME';
+    public const DB_USER = 'DB_USER';
+    public const DB_PASSWORD = 'DB_PASSWORD';
+    public const DB_HOST = 'DB_HOST';
+    public const DB_UNIX_SOCKET = 'DB_UNIX_SOCKET';
+    public const DB_PORT = 'DB_PORT';
+    public const GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY';
+    public const REDIS_SERVERS = 'REDIS_SERVERS';
+    public const REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE';
+    public const MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL';
+    public const MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL';
+    public const MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET';
+    public const DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
+    public const DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
+    public const DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
+    public const DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
+    public const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
+    public const RABBITMQ_ENABLED = 'RABBITMQ_ENABLED';
+    public const RABBITMQ_HOST = 'RABBITMQ_HOST';
+    public const RABBITMQ_PORT = 'RABBITMQ_PORT';
+    public const RABBITMQ_USER = 'RABBITMQ_USER';
+    public const RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
+    public const RABBITMQ_VHOST = 'RABBITMQ_VHOST';
+    public const DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT';
+    public const DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT';
+    public const DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT';
+    public const REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE';
+    public const REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME';
+    public const BASE_PATH = 'BASE_PATH';
+    public const PORT = 'PORT';
+    public const TASK_WORKER_NUM = 'TASK_WORKER_NUM';
+    public const WEB_WORKER_NUM = 'WEB_WORKER_NUM';
+    public const ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
+    public const TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
+    public const DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
+    public const DISABLE_TRACKING = 'DISABLE_TRACKING';
+    public const DISABLE_IP_TRACKING = 'DISABLE_IP_TRACKING';
+    public const DISABLE_REFERRER_TRACKING = 'DISABLE_REFERRER_TRACKING';
+    public const DISABLE_UA_TRACKING = 'DISABLE_UA_TRACKING';
+    public const DISABLE_TRACKING_FROM = 'DISABLE_TRACKING_FROM';
+    public const DEFAULT_SHORT_CODES_LENGTH = 'DEFAULT_SHORT_CODES_LENGTH';
+    public const IS_HTTPS_ENABLED = 'IS_HTTPS_ENABLED';
+    public const DEFAULT_DOMAIN = 'DEFAULT_DOMAIN';
+    public const AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES';
+    public const REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
+    public const VISITS_WEBHOOKS = 'VISITS_WEBHOOKS';
+    public const NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS';
+
+    /**
+     * @return string[]
+     */
+    public static function cases(): array
+    {
+        static $constants;
+        if ($constants !== null) {
+            return $constants;
+        }
+
+        $ref = new ReflectionClass(self::class);
+        return $constants = array_values($ref->getConstants(ReflectionClassConstant::IS_PUBLIC));
+    }
+
+    private function __construct(private string $envVar)
+    {
+    }
+
+    public static function __callStatic(string $name, array $arguments): self
+    {
+        if (! contains(self::cases(), $name)) {
+            throw new InvalidArgumentException('Invalid env var: "' . $name . '"');
+        }
+
+        return new self($name);
+    }
+
+    public function loadFromEnv(mixed $default = null): mixed
+    {
+        return env($this->envVar, $default);
+    }
+
+    public function existsInEnv(): bool
+    {
+        return $this->loadFromEnv() !== null;
+    }
+}