diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5f900b..cdf36c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* *Nothing* +* [#1015](https://github.com/shlinkio/shlink/issues/1015) Shlink now accepts configuration via env vars even when not using docker. + + The config generated with the installing tool still has precedence over the env vars, so it cannot be combined. Either you use the tool, or use env vars. ### Changed * [#1142](https://github.com/shlinkio/shlink/issues/1142) Replaced `doctrine/cache` package with `symfony/cache`. diff --git a/composer.json b/composer.json index 47fa16d8..4e42f2cb 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "predis/predis": "^1.1", "pugx/shortid-php": "^0.7", "ramsey/uuid": "^3.9", - "shlinkio/shlink-common": "dev-main#baef0ca as 4.0", + "shlinkio/shlink-common": "dev-main#3eacc46 as 4.0", "shlinkio/shlink-config": "^1.2", "shlinkio/shlink-event-dispatcher": "^2.1", "shlinkio/shlink-importer": "^2.3.1", diff --git a/config/autoload/delete_short_urls.global.php b/config/autoload/delete_short_urls.global.php index de2514a4..986b33ac 100644 --- a/config/autoload/delete_short_urls.global.php +++ b/config/autoload/delete_short_urls.global.php @@ -4,11 +4,15 @@ declare(strict_types=1); namespace Shlinkio\Shlink; +use function Shlinkio\Shlink\Common\env; + +use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD; + return [ 'delete_short_urls' => [ - 'visits_threshold' => 15, 'check_visits_threshold' => true, + 'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD), ], ]; diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php index c3d2ab83..08427898 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -2,24 +2,52 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Common; - use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; -return [ +use function Functional\contains; +use function Shlinkio\Shlink\Common\env; - 'entity_manager' => [ - 'orm' => [ - 'proxies_dir' => 'data/proxies', - 'load_mappings_using_functional_style' => true, - 'default_repository_classname' => EntitySpecificationRepository::class, +return (static function (): array { + $driver = env('DB_DRIVER'); + $isMysqlCompatible = contains(['maria', 'mysql'], $driver); + + $resolveDriver = static fn () => match ($driver) { + 'postgres' => 'pdo_pgsql', + 'mssql' => 'pdo_sqlsrv', + default => 'pdo_mysql', + }; + $resolveDefaultPort = static fn () => match ($driver) { + 'postgres' => '5432', + 'mssql' => '1433', + default => '3306', + }; + $resolveConnection = static fn () => match (true) { + $driver === null || $driver === 'sqlite' => [ + 'driver' => 'pdo_sqlite', + 'path' => 'data/database.sqlite', ], - 'connection' => [ - 'user' => '', - 'password' => '', - 'dbname' => 'shlink', + default => [ + 'driver' => $resolveDriver(), + 'dbname' => env('DB_NAME', 'shlink'), + 'user' => env('DB_USER'), + 'password' => env('DB_PASSWORD'), + 'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null), + 'port' => env('DB_PORT', $resolveDefaultPort()), + 'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null, 'charset' => 'utf8', ], - ], + }; -]; + return [ + + 'entity_manager' => [ + 'orm' => [ + 'proxies_dir' => 'data/proxies', + 'load_mappings_using_functional_style' => true, + 'default_repository_classname' => EntitySpecificationRepository::class, + ], + 'connection' => $resolveConnection(), + ], + + ]; +})(); diff --git a/config/autoload/geolite2.global.php b/config/autoload/geolite2.global.php index 83702ca3..3d8f0848 100644 --- a/config/autoload/geolite2.global.php +++ b/config/autoload/geolite2.global.php @@ -2,12 +2,14 @@ declare(strict_types=1); +use function Shlinkio\Shlink\Common\env; + return [ 'geolite2' => [ 'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb', 'temp_dir' => __DIR__ . '/../../data', - 'license_key' => 'G4Lm0C60yJsnkdPi', // Deprecated. Remove hardcoded license on v3 + 'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'), // Deprecated. Remove hardcoded license on v3 ], ]; diff --git a/config/autoload/locks.global.php b/config/autoload/locks.global.php index 26b31e15..35f4e90c 100644 --- a/config/autoload/locks.global.php +++ b/config/autoload/locks.global.php @@ -4,10 +4,11 @@ declare(strict_types=1); use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Predis\ClientInterface as PredisClient; -use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory; use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory; use Symfony\Component\Lock; +use function Shlinkio\Shlink\Common\env; + use const Shlinkio\Shlink\Core\LOCAL_LOCK_FACTORY; return [ @@ -24,16 +25,12 @@ return [ LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class, ], 'aliases' => [ - // With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default - 'lock_store' => 'local_lock_store', + 'lock_store' => env('REDIS_SERVERS') === null ? 'local_lock_store' : 'redis_lock_store', 'redis_lock_store' => Lock\Store\RedisStore::class, 'local_lock_store' => Lock\Store\FlockStore::class, ], 'delegators' => [ - Lock\Store\RedisStore::class => [ - RetryLockStoreDelegatorFactory::class, - ], Lock\LockFactory::class => [ LoggerAwareDelegatorFactory::class, ], diff --git a/config/autoload/mercure.global.php b/config/autoload/mercure.global.php index 72fafe58..aff8c6ee 100644 --- a/config/autoload/mercure.global.php +++ b/config/autoload/mercure.global.php @@ -7,30 +7,36 @@ use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider; use Symfony\Component\Mercure\Hub; use Symfony\Component\Mercure\HubInterface; -return [ +use function Shlinkio\Shlink\Common\env; - 'mercure' => [ - 'public_hub_url' => null, - 'internal_hub_url' => null, - 'jwt_secret' => null, - 'jwt_issuer' => 'Shlink', - ], +return (static function (): array { + $publicUrl = env('MERCURE_PUBLIC_HUB_URL'); - 'dependencies' => [ - 'delegators' => [ - LcobucciJwtProvider::class => [ - LazyServiceFactory::class, + return [ + + 'mercure' => [ + 'public_hub_url' => $publicUrl, + 'internal_hub_url' => env('MERCURE_INTERNAL_HUB_URL', $publicUrl), + 'jwt_secret' => env('MERCURE_JWT_SECRET'), + 'jwt_issuer' => 'Shlink', + ], + + 'dependencies' => [ + 'delegators' => [ + LcobucciJwtProvider::class => [ + LazyServiceFactory::class, + ], + Hub::class => [ + LazyServiceFactory::class, + ], ], - Hub::class => [ - LazyServiceFactory::class, + 'lazy_services' => [ + 'class_map' => [ + LcobucciJwtProvider::class => LcobucciJwtProvider::class, + Hub::class => HubInterface::class, + ], ], ], - 'lazy_services' => [ - 'class_map' => [ - LcobucciJwtProvider::class => LcobucciJwtProvider::class, - Hub::class => HubInterface::class, - ], - ], - ], -]; + ]; +})(); diff --git a/config/autoload/redirects.global.php b/config/autoload/redirects.global.php index 173c435c..0707f1e4 100644 --- a/config/autoload/redirects.global.php +++ b/config/autoload/redirects.global.php @@ -2,12 +2,14 @@ declare(strict_types=1); +use function Shlinkio\Shlink\Common\env; + return [ 'not_found_redirects' => [ - 'invalid_short_url' => null, - 'regular_404' => null, - 'base_url' => null, + 'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'), + 'regular_404' => env('REGULAR_404_REDIRECT_TO'), + 'base_url' => env('BASE_URL_REDIRECT_TO'), ], ]; diff --git a/config/autoload/redis.global.php b/config/autoload/redis.global.php new file mode 100644 index 00000000..a47d814c --- /dev/null +++ b/config/autoload/redis.global.php @@ -0,0 +1,20 @@ + [], + default => [ + 'cache' => [ + 'redis' => [ + 'servers' => $redisServers, + ], + ], + ], + }; +})(); diff --git a/config/autoload/router.global.php b/config/autoload/router.global.php index d45ee330..a6c6d5f0 100644 --- a/config/autoload/router.global.php +++ b/config/autoload/router.global.php @@ -4,10 +4,12 @@ declare(strict_types=1); use Mezzio\Router\FastRouteRouter; +use function Shlinkio\Shlink\Common\env; + return [ 'router' => [ - 'base_path' => '', + 'base_path' => env('BASE_PATH', ''), 'fastroute' => [ FastRouteRouter::CONFIG_CACHE_ENABLED => true, diff --git a/config/autoload/swoole.global.php b/config/autoload/swoole.global.php index 29c1ea37..3db4cf5c 100644 --- a/config/autoload/swoole.global.php +++ b/config/autoload/swoole.global.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use function Shlinkio\Shlink\Common\env; + return [ 'mezzio-swoole' => [ @@ -10,11 +12,12 @@ return [ 'swoole-http-server' => [ 'host' => '0.0.0.0', + 'port' => (int) env('PORT', 8080), 'process-name' => 'shlink', 'options' => [ - 'worker_num' => 16, - 'task_worker_num' => 16, + 'worker_num' => (int) env('WEB_WORKER_NUM', 16), + 'task_worker_num' => (int) env('TASK_WORKER_NUM', 16), ], ], ], diff --git a/config/autoload/tracking.global.php b/config/autoload/tracking.global.php index 4fdf0ba6..5ef0eaca 100644 --- a/config/autoload/tracking.global.php +++ b/config/autoload/tracking.global.php @@ -2,30 +2,32 @@ declare(strict_types=1); +use function Shlinkio\Shlink\Common\env; + 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' => true, + 'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true), // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence - 'track_orphan_visits' => true, + 'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true), // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence - 'disable_track_param' => null, + 'disable_track_param' => env('DISABLE_TRACK_PARAM'), // If true, visits will not be tracked at all - 'disable_tracking' => false, + 'disable_tracking' => (bool) env('DISABLE_TRACKING', false), // If true, visits will be tracked, but neither the IP address, nor the location will be resolved - 'disable_ip_tracking' => false, + 'disable_ip_tracking' => (bool) env('DISABLE_IP_TRACKING', false), // If true, the referrer will not be tracked - 'disable_referrer_tracking' => false, + 'disable_referrer_tracking' => (bool) env('DISABLE_REFERRER_TRACKING', false), // If true, the user agent will not be tracked - 'disable_ua_tracking' => false, + 'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false), ], ]; diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 4a6afbc2..35f95ec6 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -2,26 +2,35 @@ declare(strict_types=1); +use function Shlinkio\Shlink\Common\env; + use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME; use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; +use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH; -return [ +return (static function (): array { + $webhooks = env('VISITS_WEBHOOKS'); + $shortCodesLength = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH); + $shortCodesLength = $shortCodesLength < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $shortCodesLength; - 'url_shortener' => [ - 'domain' => [ - 'schema' => 'https', - 'hostname' => '', + return [ + + 'url_shortener' => [ + 'domain' => [ + 'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'), + 'hostname' => env('SHORT_DOMAIN_HOST', ''), + ], + 'validate_url' => (bool) env('VALIDATE_URLS', false), // Deprecated + 'visits_webhooks' => $webhooks === null ? [] : explode(',', $webhooks), + 'default_short_codes_length' => $shortCodesLength, + 'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false), + 'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false), + + // TODO Move these two options to their own config namespace. Maybe "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), ], - 'validate_url' => false, // Deprecated - 'visits_webhooks' => [], - 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, - 'auto_resolve_titles' => false, - 'append_extra_path' => false, - // TODO Move these two options to their own config namespace. Maybe "redirects". - 'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE, - 'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME, - ], - -]; + ]; +})(); diff --git a/config/config.php b/config/config.php index 2b562874..887aa365 100644 --- a/config/config.php +++ b/config/config.php @@ -8,7 +8,7 @@ use Laminas\ConfigAggregator; use Laminas\Diactoros; use Mezzio; use Mezzio\ProblemDetails; -use Mezzio\Swoole\ConfigProvider as SwooleConfigProvider; +use Mezzio\Swoole; use function class_exists; use function Shlinkio\Shlink\Common\env; @@ -17,7 +17,7 @@ return (new ConfigAggregator\ConfigAggregator([ Mezzio\ConfigProvider::class, Mezzio\Router\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class, - class_exists(SwooleConfigProvider::class) ? SwooleConfigProvider::class : new ConfigAggregator\ArrayProvider([]), + class_exists(Swoole\ConfigProvider::class) ? Swoole\ConfigProvider::class : new ConfigAggregator\ArrayProvider([]), ProblemDetails\ConfigProvider::class, Diactoros\ConfigProvider::class, Common\ConfigProvider::class, @@ -31,6 +31,7 @@ return (new ConfigAggregator\ConfigAggregator([ new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), env('APP_ENV') === 'test' ? new ConfigAggregator\PhpFileProvider('config/test/*.global.php') + // Deprecated. When the SimplifiedConfigParser is removed, load only generated_config.php here : new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'), ], 'data/cache/app_config.php', [ Core\Config\SimplifiedConfigParser::class, diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index d4526f50..73cc3fdc 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -7,128 +7,8 @@ namespace Shlinkio\Shlink; use Monolog\Handler\StreamHandler; use Monolog\Logger; -use function explode; -use function Functional\contains; -use function Shlinkio\Shlink\Common\env; - -use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD; -use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME; -use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; -use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; -use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH; - -$helper = new class { - private const DB_DRIVERS_MAP = [ - 'mysql' => 'pdo_mysql', - 'maria' => 'pdo_mysql', - 'postgres' => 'pdo_pgsql', - 'mssql' => 'pdo_sqlsrv', - ]; - private const DB_PORTS_MAP = [ - 'mysql' => '3306', - 'maria' => '3306', - 'postgres' => '5432', - 'mssql' => '1433', - ]; - - public function getDbConfig(): array - { - $driver = env('DB_DRIVER'); - $isMysql = contains(['maria', 'mysql'], $driver); - if ($driver === null || $driver === 'sqlite') { - return [ - 'driver' => 'pdo_sqlite', - 'path' => 'data/database.sqlite', - ]; - } - - return [ - 'driver' => self::DB_DRIVERS_MAP[$driver], - 'dbname' => env('DB_NAME', 'shlink'), - 'user' => env('DB_USER'), - 'password' => env('DB_PASSWORD'), - 'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null), - 'port' => env('DB_PORT', self::DB_PORTS_MAP[$driver]), - 'unix_socket' => $isMysql ? env('DB_UNIX_SOCKET') : null, - ]; - } - - public function getNotFoundRedirectsConfig(): array - { - return [ - 'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'), - 'regular_404' => env('REGULAR_404_REDIRECT_TO'), - 'base_url' => env('BASE_URL_REDIRECT_TO'), - ]; - } - - public function getVisitsWebhooks(): array - { - $webhooks = env('VISITS_WEBHOOKS'); - return $webhooks === null ? [] : explode(',', $webhooks); - } - - public function getRedisConfig(): ?array - { - $redisServers = env('REDIS_SERVERS'); - return $redisServers === null ? null : ['servers' => $redisServers]; - } - - public function getDefaultShortCodesLength(): int - { - $value = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH); - return $value < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $value; - } - - public function getMercureConfig(): array - { - $publicUrl = env('MERCURE_PUBLIC_HUB_URL'); - - return [ - 'public_hub_url' => $publicUrl, - 'internal_hub_url' => env('MERCURE_INTERNAL_HUB_URL', $publicUrl), - 'jwt_secret' => env('MERCURE_JWT_SECRET'), - ]; - } -}; - return [ - 'delete_short_urls' => [ - 'check_visits_threshold' => true, - 'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD), - ], - - 'entity_manager' => [ - 'connection' => $helper->getDbConfig(), - ], - - 'url_shortener' => [ - 'domain' => [ - 'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'), - 'hostname' => env('SHORT_DOMAIN_HOST', ''), - ], - 'validate_url' => (bool) env('VALIDATE_URLS', false), - 'visits_webhooks' => $helper->getVisitsWebhooks(), - 'default_short_codes_length' => $helper->getDefaultShortCodesLength(), - 'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false), - 'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE), - 'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME), - 'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false), - ], - - 'tracking' => [ - 'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true), - 'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true), - 'disable_track_param' => env('DISABLE_TRACK_PARAM'), - 'disable_tracking' => (bool) env('DISABLE_TRACKING', false), - 'disable_ip_tracking' => (bool) env('DISABLE_IP_TRACKING', false), - 'disable_referrer_tracking' => (bool) env('DISABLE_REFERRER_TRACKING', false), - 'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false), - ], - - 'not_found_redirects' => $helper->getNotFoundRedirectsConfig(), - 'logger' => [ 'Shlink' => [ 'handlers' => [ @@ -143,34 +23,4 @@ return [ ], ], - 'dependencies' => [ - 'aliases' => env('REDIS_SERVERS') === null ? [] : [ - 'lock_store' => 'redis_lock_store', - ], - ], - - 'cache' => [ - 'redis' => $helper->getRedisConfig(), - ], - - 'router' => [ - 'base_path' => env('BASE_PATH', ''), - ], - - 'mezzio-swoole' => [ - 'swoole-http-server' => [ - 'port' => (int) env('PORT', 8080), - 'options' => [ - 'worker_num' => (int) env('WEB_WORKER_NUM', 16), - 'task_worker_num' => (int) env('TASK_WORKER_NUM', 16), - ], - ], - ], - - 'geolite2' => [ - 'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'), // Deprecated. Remove hardcoded license on v3 - ], - - 'mercure' => $helper->getMercureConfig(), - ];