diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ccb54c..385066e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0'] + php-version: ['8.1'] command: ['cs', 'stan', 'swagger:validate'] steps: - name: Checkout code @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0', '8.1'] + php-version: ['8.1'] test-group: ['unit', 'api'] steps: - name: Checkout code @@ -51,7 +51,7 @@ jobs: - run: composer install --no-interaction --prefer-dist - run: composer test:${{ matrix.test-group }}:ci - uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.0' }} + if: ${{ matrix.php-version == '8.1' }} with: name: coverage-${{ matrix.test-group }} path: | @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0', '8.1'] + php-version: ['8.1'] platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms'] env: LC_ALL: C @@ -91,7 +91,7 @@ jobs: run: composer test:db:${{ matrix.platform }} - name: Upload code coverage uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.0' && matrix.platform == 'sqlite:ci' }} + if: ${{ matrix.php-version == '8.1' && matrix.platform == 'sqlite:ci' }} with: name: coverage-db path: | @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0', '8.1'] + php-version: ['8.1'] test-group: ['unit', 'db', 'api'] steps: - name: Checkout code @@ -136,7 +136,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0'] + php-version: ['8.1'] steps: - name: Checkout code uses: actions/checkout@v2 @@ -152,8 +152,8 @@ jobs: - run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov - run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov - run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov - - run: wget https://phar.phpunit.de/phpcov-8.2.0.phar - - run: php phpcov-8.2.0.phar merge build --clover build/clover.xml + - run: wget https://phar.phpunit.de/phpcov-8.2.1.phar + - run: php phpcov-8.2.1.phar merge build --clover build/clover.xml - name: Publish coverage uses: codecov/codecov-action@v1 with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6d50221a..f872ebee 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0', '8.1'] + php-version: ['8.1'] swoole: ['yes', 'no'] steps: - name: Checkout code @@ -53,8 +53,8 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: [ '8.0', '8.1' ] - swoole: [ 'yes', 'no' ] + php-version: ['8.1'] + swoole: ['yes', 'no'] steps: - uses: geekyeggo/delete-artifact@v1 with: diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 5b97ee6c..51907814 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php-version: ['8.0'] + php-version: ['8.1'] steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 882bd2ed..c29a817e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] +### Added +* *Nothing* + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* [#1280](https://github.com/shlinkio/shlink/issues/1280) Dropped support for PHP 8.0 + +### Fixed +* *Nothing* + + ## [3.1.0] - 2022-04-23 ### Added * [#1294](https://github.com/shlinkio/shlink/issues/1294) Allowed to provide a specific domain when importing URLs from YOURLS. diff --git a/README.md b/README.md index 6f4afd37..f72d9f92 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The idea is that you can just generate a container using the image and provide t First, make sure the host where you are going to run shlink fulfills these requirements: -* PHP 8.0 or 8.1 +* PHP 8.1 * The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath. * apcu extension is recommended if you don't plan to use openswoole. * xml extension is required if you want to generate QR codes in svg format. diff --git a/composer.json b/composer.json index 2446aace..1e8c580b 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-json": "*", "ext-pdo": "*", "akrabat/ip-address-middleware": "^2.1", diff --git a/config/autoload/delete_short_urls.global.php b/config/autoload/delete_short_urls.global.php index 3d562f78..2d203ea1 100644 --- a/config/autoload/delete_short_urls.global.php +++ b/config/autoload/delete_short_urls.global.php @@ -7,7 +7,7 @@ namespace Shlinkio\Shlink; use Shlinkio\Shlink\Core\Config\EnvVars; return (static function (): array { - $threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD()->loadFromEnv(); + $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 d98d37dc..5a75ca6b 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars; use function Functional\contains; return (static function (): array { - $driver = EnvVars::DB_DRIVER()->loadFromEnv(); + $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' => 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, + '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 cf1f57fc..b31cfc6d 100644 --- a/config/autoload/geolite2.global.php +++ b/config/autoload/geolite2.global.php @@ -9,7 +9,7 @@ return [ 'geolite2' => [ 'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb', 'temp_dir' => __DIR__ . '/../../data', - 'license_key' => EnvVars::GEOLITE_LICENSE_KEY()->loadFromEnv(), + 'license_key' => EnvVars::GEOLITE_LICENSE_KEY->loadFromEnv(), ], ]; diff --git a/config/autoload/locks.global.php b/config/autoload/locks.global.php index bdbdb8e5..9b014496 100644 --- a/config/autoload/locks.global.php +++ b/config/autoload/locks.global.php @@ -24,7 +24,7 @@ return [ LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class, ], 'aliases' => [ - 'lock_store' => EnvVars::REDIS_SERVERS()->existsInEnv() ? 'redis_lock_store' : 'local_lock_store', + 'lock_store' => EnvVars::REDIS_SERVERS->existsInEnv() ? 'redis_lock_store' : 'local_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 ba261369..67143919 100644 --- a/config/autoload/mercure.global.php +++ b/config/autoload/mercure.global.php @@ -9,14 +9,14 @@ use Symfony\Component\Mercure\Hub; use Symfony\Component\Mercure\HubInterface; return (static function (): array { - $publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL()->loadFromEnv(); + $publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL->loadFromEnv(); return [ 'mercure' => [ 'public_hub_url' => $publicUrl, - 'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL()->loadFromEnv($publicUrl), - 'jwt_secret' => EnvVars::MERCURE_JWT_SECRET()->loadFromEnv(), + '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 d72198af..dc4f5f9e 100644 --- a/config/autoload/qr-codes.global.php +++ b/config/autoload/qr-codes.global.php @@ -13,13 +13,13 @@ use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE; return [ 'qr_codes' => [ - '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( + '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( + '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 faa5f569..a9764c8c 100644 --- a/config/autoload/rabbit.global.php +++ b/config/autoload/rabbit.global.php @@ -10,12 +10,12 @@ use Shlinkio\Shlink\Core\Config\EnvVars; return [ 'rabbitmq' => [ - '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('/'), + '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 08439b2a..426bb2ac 100644 --- a/config/autoload/redirects.global.php +++ b/config/autoload/redirects.global.php @@ -10,14 +10,14 @@ use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE; return [ 'not_found_redirects' => [ - '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(), + '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) EnvVars::REDIRECT_STATUS_CODE()->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE), - 'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME()->loadFromEnv( + '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 f87d77f3..0133d1b1 100644 --- a/config/autoload/redis.global.php +++ b/config/autoload/redis.global.php @@ -5,7 +5,7 @@ declare(strict_types=1); use Shlinkio\Shlink\Core\Config\EnvVars; return (static function (): array { - $redisServers = EnvVars::REDIS_SERVERS()->loadFromEnv(); + $redisServers = EnvVars::REDIS_SERVERS->loadFromEnv(); return match ($redisServers) { null => [], @@ -13,7 +13,7 @@ return (static function (): array { 'cache' => [ 'redis' => [ 'servers' => $redisServers, - 'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE()->loadFromEnv(), + 'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(), ], ], ], diff --git a/config/autoload/router.global.php b/config/autoload/router.global.php index fd1f9525..8b5e856e 100644 --- a/config/autoload/router.global.php +++ b/config/autoload/router.global.php @@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars; return [ 'router' => [ - 'base_path' => EnvVars::BASE_PATH()->loadFromEnv(''), + '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 987c967e..36cba24f 100644 --- a/config/autoload/swoole.global.php +++ b/config/autoload/swoole.global.php @@ -7,7 +7,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars; use const Shlinkio\Shlink\MIN_TASK_WORKERS; return (static function (): array { - $taskWorkers = (int) EnvVars::TASK_WORKER_NUM()->loadFromEnv(16); + $taskWorkers = (int) EnvVars::TASK_WORKER_NUM->loadFromEnv(16); return [ @@ -17,11 +17,11 @@ return (static function (): array { 'swoole-http-server' => [ 'host' => '0.0.0.0', - 'port' => (int) EnvVars::PORT()->loadFromEnv(8080), + 'port' => (int) EnvVars::PORT->loadFromEnv(8080), 'process-name' => 'shlink', 'options' => [ - 'worker_num' => (int) EnvVars::WEB_WORKER_NUM()->loadFromEnv(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 b2596830..0637301a 100644 --- a/config/autoload/tracking.global.php +++ b/config/autoload/tracking.global.php @@ -9,28 +9,28 @@ 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) EnvVars::ANONYMIZE_REMOTE_ADDR()->loadFromEnv(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) EnvVars::TRACK_ORPHAN_VISITS()->loadFromEnv(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' => EnvVars::DISABLE_TRACK_PARAM()->loadFromEnv(), + 'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(), // If true, visits will not be tracked at all - 'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING()->loadFromEnv(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) EnvVars::DISABLE_IP_TRACKING()->loadFromEnv(false), + 'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false), // If true, the referrer will not be tracked - 'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING()->loadFromEnv(false), + 'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false), // If true, the user agent will not be tracked - 'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING()->loadFromEnv(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' => EnvVars::DISABLE_TRACKING_FROM()->loadFromEnv(), + '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 58c12f05..bdd257d5 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -9,7 +9,7 @@ use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH; return (static function (): array { $shortCodesLength = max( - (int) EnvVars::DEFAULT_SHORT_CODES_LENGTH()->loadFromEnv(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' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain - 'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED()->loadFromEnv(true)) ? 'https' : 'http', - 'hostname' => EnvVars::DEFAULT_DOMAIN()->loadFromEnv(''), + 'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv(true)) ? 'https' : 'http', + 'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''), ], 'default_short_codes_length' => $shortCodesLength, - 'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES()->loadFromEnv(false), - 'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH()->loadFromEnv(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 5de7c53b..e72c4904 100644 --- a/config/autoload/webhooks.global.php +++ b/config/autoload/webhooks.global.php @@ -6,14 +6,14 @@ use Shlinkio\Shlink\Core\Config\EnvVars; // Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0 return (static function (): array { - $webhooks = EnvVars::VISITS_WEBHOOKS()->loadFromEnv(); + $webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv(); return [ 'visits_webhooks' => [ 'webhooks' => $webhooks === null ? [] : explode(',', $webhooks), 'notify_orphan_visits_to_webhooks' => - (bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()->loadFromEnv(false), + (bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false), ], ]; diff --git a/config/container.php b/config/container.php index 074502cd..6e95e84d 100644 --- a/config/container.php +++ b/config/container.php @@ -13,7 +13,7 @@ chdir(dirname(__DIR__)); require 'vendor/autoload.php'; // This is one of the first files loaded. Configure the timezone here -date_default_timezone_set(EnvVars::TIMEZONE()->loadFromEnv(date_default_timezone_get())); +date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get())); // This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name // It needs to be placed here as individual config files will not be loaded once config is cached diff --git a/data/infra/examples/nginx-vhost.conf b/data/infra/examples/nginx-vhost.conf index 80ff8afd..5e05481a 100644 --- a/data/infra/examples/nginx-vhost.conf +++ b/data/infra/examples/nginx-vhost.conf @@ -11,7 +11,7 @@ server { location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; + fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_index index.php; include fastcgi.conf; } diff --git a/data/migrations/Version20210207100807.php b/data/migrations/Version20210207100807.php index 706132cc..cd0b0b12 100644 --- a/data/migrations/Version20210207100807.php +++ b/data/migrations/Version20210207100807.php @@ -8,8 +8,8 @@ use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; -use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; final class Version20210207100807 extends AbstractMigration { @@ -27,7 +27,7 @@ final class Version20210207100807 extends AbstractMigration ]); $visits->addColumn('type', Types::STRING, [ 'length' => 255, - 'default' => Visit::TYPE_VALID_SHORT_URL, + 'default' => VisitType::VALID_SHORT_URL->value, ]); } diff --git a/module/CLI/src/Command/Api/GenerateKeyCommand.php b/module/CLI/src/Command/Api/GenerateKeyCommand.php index 2655d1fb..b24619ef 100644 --- a/module/CLI/src/Command/Api/GenerateKeyCommand.php +++ b/module/CLI/src/Command/Api/GenerateKeyCommand.php @@ -73,13 +73,16 @@ class GenerateKeyCommand extends Command $authorOnly, 'a', InputOption::VALUE_NONE, - sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS), + sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS->value), ) ->addOption( $domainOnly, 'd', InputOption::VALUE_REQUIRED, - sprintf('Adds the "%s" role to the new API key, with the domain provided.', Role::DOMAIN_SPECIFIC), + sprintf( + 'Adds the "%s" role to the new API key, with the domain provided.', + Role::DOMAIN_SPECIFIC->value, + ), ) ->setHelp($help); } @@ -99,7 +102,7 @@ class GenerateKeyCommand extends Command if (! $apiKey->isAdmin()) { ShlinkTable::default($io)->render( ['Role name', 'Role metadata'], - $apiKey->mapRoles(fn (string $name, array $meta) => [$name, arrayToString($meta, 0)]), + $apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]), null, 'Roles', ); diff --git a/module/CLI/src/Command/Api/ListKeysCommand.php b/module/CLI/src/Command/Api/ListKeysCommand.php index 0a331086..0e98af31 100644 --- a/module/CLI/src/Command/Api/ListKeysCommand.php +++ b/module/CLI/src/Command/Api/ListKeysCommand.php @@ -60,10 +60,10 @@ class ListKeysCommand extends Command } $rowData[] = $expiration?->toAtomString() ?? '-'; $rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles( - fn (string $roleName, array $meta) => + fn (Role $role, array $meta) => empty($meta) - ? Role::toFriendlyName($roleName) - : sprintf('%s: %s', Role::toFriendlyName($roleName), Role::domainAuthorityFromMeta($meta)), + ? Role::toFriendlyName($role) + : sprintf('%s: %s', Role::toFriendlyName($role), Role::domainAuthorityFromMeta($meta)), )); return $rowData; diff --git a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php index 90cfd1f7..c546fd5b 100644 --- a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php +++ b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php @@ -53,7 +53,7 @@ class DomainRedirectsCommand extends Command /** @var string[] $availableDomains */ $availableDomains = invoke( - filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault()), + filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault), 'toString', ); if (empty($availableDomains)) { diff --git a/module/CLI/src/Command/Domain/ListDomainsCommand.php b/module/CLI/src/Command/Domain/ListDomainsCommand.php index 447bf92f..8f2ee22c 100644 --- a/module/CLI/src/Command/Domain/ListDomainsCommand.php +++ b/module/CLI/src/Command/Domain/ListDomainsCommand.php @@ -48,12 +48,12 @@ class ListDomainsCommand extends Command $table->render( $showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields, map($domains, function (DomainItem $domain) use ($showRedirects) { - $commonValues = [$domain->toString(), $domain->isDefault() ? 'Yes' : 'No']; + $commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No']; return $showRedirects ? [ ...$commonValues, - $this->notFoundRedirectsToString($domain->notFoundRedirectConfig()), + $this->notFoundRedirectsToString($domain->notFoundRedirectConfig), ] : $commonValues; }), diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php index fc4e8331..db1b1dfd 100644 --- a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php @@ -81,6 +81,6 @@ class DeleteShortUrlCommand extends Command private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void { $this->deleteShortUrlService->deleteByShortCode($identifier, $ignoreThreshold); - $io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode())); + $io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode)); } } diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index ebc9e783..fc0f19a0 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -13,6 +13,7 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -120,9 +121,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand $page = (int) $input->getOption('page'); $searchTerm = $input->getOption('search-term'); $tags = $input->getOption('tags'); - $tagsMode = $input->getOption('including-all-tags') === true - ? ShortUrlsParams::TAGS_MODE_ALL - : ShortUrlsParams::TAGS_MODE_ANY; + $tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value; $tags = ! empty($tags) ? explode(',', $tags) : []; $all = $input->getOption('all'); $startDate = $this->getStartDateOption($input, $output); diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php index 9c7269fa..cd820169 100644 --- a/module/CLI/src/Command/Tag/ListTagsCommand.php +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -46,7 +46,7 @@ class ListTagsCommand extends Command return map( $tags, - static fn (TagInfo $tagInfo) => [$tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()], + static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsCount], ); } } diff --git a/module/CLI/src/Command/Util/AbstractLockedCommand.php b/module/CLI/src/Command/Util/AbstractLockedCommand.php index 9482694b..5f3a33ea 100644 --- a/module/CLI/src/Command/Util/AbstractLockedCommand.php +++ b/module/CLI/src/Command/Util/AbstractLockedCommand.php @@ -22,11 +22,11 @@ abstract class AbstractLockedCommand extends Command final protected function execute(InputInterface $input, OutputInterface $output): ?int { $lockConfig = $this->getLockConfig(); - $lock = $this->locker->createLock($lockConfig->lockName(), $lockConfig->ttl(), $lockConfig->isBlocking()); + $lock = $this->locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking); - if (! $lock->acquire($lockConfig->isBlocking())) { + if (! $lock->acquire($lockConfig->isBlocking)) { $output->writeln( - sprintf('Command "%s" is already in progress. Skipping.', $lockConfig->lockName()), + sprintf('Command "%s" is already in progress. Skipping.', $lockConfig->lockName), ); return ExitCodes::EXIT_WARNING; } diff --git a/module/CLI/src/Command/Util/LockedCommandConfig.php b/module/CLI/src/Command/Util/LockedCommandConfig.php index f053d99a..8e357329 100644 --- a/module/CLI/src/Command/Util/LockedCommandConfig.php +++ b/module/CLI/src/Command/Util/LockedCommandConfig.php @@ -9,9 +9,9 @@ final class LockedCommandConfig public const DEFAULT_TTL = 600.0; // 10 minutes private function __construct( - private string $lockName, - private bool $isBlocking, - private float $ttl = self::DEFAULT_TTL, + public readonly string $lockName, + public readonly bool $isBlocking, + public readonly float $ttl = self::DEFAULT_TTL, ) { } @@ -24,19 +24,4 @@ final class LockedCommandConfig { return new self($lockName, false); } - - public function lockName(): string - { - return $this->lockName; - } - - public function isBlocking(): bool - { - return $this->isBlocking; - } - - public function ttl(): float - { - return $this->ttl; - } } diff --git a/module/CLI/src/Exception/InvalidRoleConfigException.php b/module/CLI/src/Exception/InvalidRoleConfigException.php index 51adb234..ae483766 100644 --- a/module/CLI/src/Exception/InvalidRoleConfigException.php +++ b/module/CLI/src/Exception/InvalidRoleConfigException.php @@ -16,7 +16,7 @@ class InvalidRoleConfigException extends InvalidArgumentException implements Exc return new self(sprintf( 'You cannot create an API key with the "%s" role attached to the default domain. ' . 'The role is currently limited to non-default domains.', - Role::DOMAIN_SPECIFIC, + Role::DOMAIN_SPECIFIC->value, )); } } diff --git a/module/CLI/src/Util/ShlinkTable.php b/module/CLI/src/Util/ShlinkTable.php index 1d4143c1..cd38e5cd 100644 --- a/module/CLI/src/Util/ShlinkTable.php +++ b/module/CLI/src/Util/ShlinkTable.php @@ -15,7 +15,7 @@ final class ShlinkTable private const DEFAULT_STYLE_NAME = 'default'; private const TABLE_TITLE_STYLE = ' %s '; - private function __construct(private Table $baseTable, private bool $withRowSeparators) + private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators) { } diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index 10a363c7..947b7443 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -36,10 +36,11 @@ class DeleteShortUrlCommandTest extends TestCase public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void { $shortCode = 'abc123'; - $deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->will( - function (): void { - }, - ); + $deleteByShortCode = $this->service->deleteByShortCode( + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + false, + )->will(function (): void { + }); $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); @@ -55,7 +56,7 @@ class DeleteShortUrlCommandTest extends TestCase public function invalidShortCodePrintsMessage(): void { $shortCode = 'abc123'; - $identifier = new ShortUrlIdentifier($shortCode); + $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode); $deleteByShortCode = $this->service->deleteByShortCode($identifier, false)->willThrow( Exception\ShortUrlNotFoundException::fromNotFound($identifier), ); @@ -77,7 +78,7 @@ class DeleteShortUrlCommandTest extends TestCase string $expectedMessage, ): void { $shortCode = 'abc123'; - $identifier = new ShortUrlIdentifier($shortCode); + $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode); $deleteByShortCode = $this->service->deleteByShortCode($identifier, Argument::type('bool'))->will( function (array $args) use ($shortCode): void { $ignoreThreshold = array_pop($args); @@ -114,12 +115,13 @@ class DeleteShortUrlCommandTest extends TestCase public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void { $shortCode = 'abc123'; - $deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->willThrow( - Exception\DeleteShortUrlException::fromVisitsThreshold( - 10, - ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), - ), - ); + $deleteByShortCode = $this->service->deleteByShortCode( + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + false, + )->willThrow(Exception\DeleteShortUrlException::fromVisitsThreshold( + 10, + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + )); $this->commandTester->setInputs(['no']); $this->commandTester->execute(['shortCode' => $shortCode]); diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php index ca9e0981..7a884c89 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php @@ -44,7 +44,7 @@ class GetVisitsCommandTest extends TestCase { $shortCode = 'abc123'; $this->visitsHelper->visitsForShortUrl( - new ShortUrlIdentifier($shortCode), + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), new VisitsParams(DateRange::emptyInstance()), ) ->willReturn(new Paginator(new ArrayAdapter([]))) @@ -60,7 +60,7 @@ class GetVisitsCommandTest extends TestCase $startDate = '2016-01-01'; $endDate = '2016-02-01'; $this->visitsHelper->visitsForShortUrl( - new ShortUrlIdentifier($shortCode), + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), new VisitsParams(DateRange::withStartAndEndDate(Chronos::parse($startDate), Chronos::parse($endDate))), ) ->willReturn(new Paginator(new ArrayAdapter([]))) @@ -79,7 +79,7 @@ class GetVisitsCommandTest extends TestCase $shortCode = 'abc123'; $startDate = 'foo'; $info = $this->visitsHelper->visitsForShortUrl( - new ShortUrlIdentifier($shortCode), + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), new VisitsParams(DateRange::emptyInstance()), )->willReturn(new Paginator(new ArrayAdapter([]))); @@ -100,7 +100,10 @@ class GetVisitsCommandTest extends TestCase public function outputIsProperlyGenerated(): void { $shortCode = 'abc123'; - $this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn( + $this->visitsHelper->visitsForShortUrl( + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + Argument::any(), + )->willReturn( new Paginator(new ArrayAdapter([ Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate( VisitLocation::fromGeolocation(new Location('', 'Spain', '', '', 0, 0, '')), diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index 38d3bcd3..f9d701cb 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -205,23 +206,23 @@ class ListShortUrlsCommandTest extends TestCase public function provideArgs(): iterable { - yield [[], 1, null, [], ShortUrlsParams::TAGS_MODE_ANY]; - yield [['--page' => $page = 3], $page, null, [], ShortUrlsParams::TAGS_MODE_ANY]; - yield [['--including-all-tags' => true], 1, null, [], ShortUrlsParams::TAGS_MODE_ALL]; - yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], ShortUrlsParams::TAGS_MODE_ANY]; + yield [[], 1, null, [], TagsMode::ANY->value]; + yield [['--page' => $page = 3], $page, null, [], TagsMode::ANY->value]; + yield [['--including-all-tags' => true], 1, null, [], TagsMode::ALL->value]; + yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], TagsMode::ANY->value]; yield [ ['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'], $page, $searchTerm, explode(',', $tags), - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY->value, ]; yield [ ['--start-date' => $startDate = '2019-01-01'], 1, null, [], - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY->value, $startDate, ]; yield [ @@ -229,7 +230,7 @@ class ListShortUrlsCommandTest extends TestCase 1, null, [], - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY->value, null, $endDate, ]; @@ -238,7 +239,7 @@ class ListShortUrlsCommandTest extends TestCase 1, null, [], - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY->value, $startDate, $endDate, ]; @@ -276,7 +277,7 @@ class ListShortUrlsCommandTest extends TestCase 'page' => 1, 'searchTerm' => null, 'tags' => [], - 'tagsMode' => ShortUrlsParams::TAGS_MODE_ANY, + 'tagsMode' => TagsMode::ANY->value, 'startDate' => null, 'endDate' => null, 'orderBy' => null, diff --git a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php index 2a816207..12e29eaf 100644 --- a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php @@ -37,8 +37,9 @@ class ResolveUrlCommandTest extends TestCase $shortCode = 'abc123'; $expectedUrl = 'http://domain.com/foo/bar'; $shortUrl = ShortUrl::withLongUrl($expectedUrl); - $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn($shortUrl) - ->shouldBeCalledOnce(); + $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode))->willReturn( + $shortUrl, + )->shouldBeCalledOnce(); $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); @@ -48,8 +49,8 @@ class ResolveUrlCommandTest extends TestCase /** @test */ public function incorrectShortCodeOutputsErrorMessage(): void { - $identifier = new ShortUrlIdentifier('abc123'); - $shortCode = $identifier->shortCode(); + $identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123'); + $shortCode = $identifier->shortCode; $this->urlResolver->resolveShortUrl($identifier) ->willThrow(ShortUrlNotFoundException::fromNotFound($identifier)) diff --git a/module/CLI/test/Exception/InvalidRoleConfigExceptionTest.php b/module/CLI/test/Exception/InvalidRoleConfigExceptionTest.php index 3b89b505..99c66ea4 100644 --- a/module/CLI/test/Exception/InvalidRoleConfigExceptionTest.php +++ b/module/CLI/test/Exception/InvalidRoleConfigExceptionTest.php @@ -20,7 +20,7 @@ class InvalidRoleConfigExceptionTest extends TestCase self::assertEquals(sprintf( 'You cannot create an API key with the "%s" role attached to the default domain. ' . 'The role is currently limited to non-default domains.', - Role::DOMAIN_SPECIFIC, + Role::DOMAIN_SPECIFIC->value, ), $e->getMessage()); } } diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.Visit.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.Visit.php index 969bfd1d..147c37e7 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.Visit.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.Visit.php @@ -6,9 +6,11 @@ namespace Shlinkio\Shlink\Core; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder; +use Doctrine\ORM\Mapping\Builder\FieldBuilder; use Doctrine\ORM\Mapping\ClassMetadata; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; use Shlinkio\Shlink\Core\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; return static function (ClassMetadata $metadata, array $emConfig): void { $builder = new ClassMetadataBuilder($metadata); @@ -61,10 +63,13 @@ return static function (ClassMetadata $metadata, array $emConfig): void { ->nullable() ->build(); - $builder->createField('type', Types::STRING) - ->columnName('type') - ->length(255) - ->build(); + (new FieldBuilder($builder, [ + 'fieldName' => 'type', + 'type' => Types::STRING, + 'enumType' => VisitType::class, + ]))->columnName('type') + ->length(255) + ->build(); $builder->createField('potentialBot', Types::BOOLEAN) ->columnName('potential_bot') diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 42d643d3..7c1f0e34 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -29,11 +29,11 @@ final class QrCodeParams private const SUPPORTED_FORMATS = ['png', 'svg']; private function __construct( - private int $size, - private int $margin, - private WriterInterface $writer, - private ErrorCorrectionLevelInterface $errorCorrectionLevel, - private RoundBlockSizeModeInterface $roundBlockSizeMode, + public readonly int $size, + public readonly int $margin, + public readonly WriterInterface $writer, + public readonly ErrorCorrectionLevelInterface $errorCorrectionLevel, + public readonly RoundBlockSizeModeInterface $roundBlockSizeMode, ) { } @@ -105,29 +105,4 @@ final class QrCodeParams { return strtolower(trim($param)); } - - public function size(): int - { - return $this->size; - } - - public function margin(): int - { - return $this->margin; - } - - public function writer(): WriterInterface - { - return $this->writer; - } - - public function errorCorrectionLevel(): ErrorCorrectionLevelInterface - { - return $this->errorCorrectionLevel; - } - - public function roundBlockSizeMode(): RoundBlockSizeModeInterface - { - return $this->roundBlockSizeMode; - } } diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index 7772a5c8..17bdbdfd 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -42,11 +42,11 @@ class QrCodeAction implements MiddlewareInterface $params = QrCodeParams::fromRequest($request, $this->defaultOptions); $qrCodeBuilder = Builder::create() ->data($this->stringifier->stringify($shortUrl)) - ->size($params->size()) - ->margin($params->margin()) - ->writer($params->writer()) - ->errorCorrectionLevel($params->errorCorrectionLevel()) - ->roundBlockSizeMode($params->roundBlockSizeMode()); + ->size($params->size) + ->margin($params->margin) + ->writer($params->writer) + ->errorCorrectionLevel($params->errorCorrectionLevel) + ->roundBlockSizeMode($params->roundBlockSizeMode); return new QrCodeResponse($qrCodeBuilder->build()); } diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 112b7599..e26e3097 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -2,155 +2,70 @@ declare(strict_types=1); +// phpcs:disable +// TODO Enable coding style checks again once code sniffer 3.7 is released https://github.com/squizlabs/PHP_CodeSniffer/issues/3474 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 after dropping PHP 8.0 support - -/** - * @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 TIMEZONE() - * @method static EnvVars VISITS_WEBHOOKS() - * @method static EnvVars NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS() - */ -final class EnvVars +enum EnvVars: string { - 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 TIMEZONE = 'TIMEZONE'; + case DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD'; + case DB_DRIVER = 'DB_DRIVER'; + case DB_NAME = 'DB_NAME'; + case DB_USER = 'DB_USER'; + case DB_PASSWORD = 'DB_PASSWORD'; + case DB_HOST = 'DB_HOST'; + case DB_UNIX_SOCKET = 'DB_UNIX_SOCKET'; + case DB_PORT = 'DB_PORT'; + case GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY'; + case REDIS_SERVERS = 'REDIS_SERVERS'; + case REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE'; + case MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL'; + case MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL'; + case MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET'; + case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE'; + case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN'; + case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT'; + case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION'; + case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE'; + case RABBITMQ_ENABLED = 'RABBITMQ_ENABLED'; + case RABBITMQ_HOST = 'RABBITMQ_HOST'; + case RABBITMQ_PORT = 'RABBITMQ_PORT'; + case RABBITMQ_USER = 'RABBITMQ_USER'; + case RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD'; + case RABBITMQ_VHOST = 'RABBITMQ_VHOST'; + case DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT'; + case DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT'; + case DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT'; + case REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE'; + case REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME'; + case BASE_PATH = 'BASE_PATH'; + case PORT = 'PORT'; + case TASK_WORKER_NUM = 'TASK_WORKER_NUM'; + case WEB_WORKER_NUM = 'WEB_WORKER_NUM'; + case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR'; + case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS'; + case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM'; + case DISABLE_TRACKING = 'DISABLE_TRACKING'; + case DISABLE_IP_TRACKING = 'DISABLE_IP_TRACKING'; + case DISABLE_REFERRER_TRACKING = 'DISABLE_REFERRER_TRACKING'; + case DISABLE_UA_TRACKING = 'DISABLE_UA_TRACKING'; + case DISABLE_TRACKING_FROM = 'DISABLE_TRACKING_FROM'; + case DEFAULT_SHORT_CODES_LENGTH = 'DEFAULT_SHORT_CODES_LENGTH'; + case IS_HTTPS_ENABLED = 'IS_HTTPS_ENABLED'; + case DEFAULT_DOMAIN = 'DEFAULT_DOMAIN'; + case AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES'; + case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH'; + case TIMEZONE = 'TIMEZONE'; /** @deprecated */ - public const VISITS_WEBHOOKS = 'VISITS_WEBHOOKS'; + case VISITS_WEBHOOKS = 'VISITS_WEBHOOKS'; /** @deprecated */ - 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); - } + case NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS'; public function loadFromEnv(mixed $default = null): mixed { - return env($this->envVar, $default); + return env($this->value, $default); } public function existsInEnv(): bool diff --git a/module/Core/src/Config/NotFoundRedirectResolver.php b/module/Core/src/Config/NotFoundRedirectResolver.php index caa100c3..3ab2e740 100644 --- a/module/Core/src/Config/NotFoundRedirectResolver.php +++ b/module/Core/src/Config/NotFoundRedirectResolver.php @@ -13,7 +13,9 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType; use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface; use function Functional\compose; +use function Functional\id; use function str_replace; +use function urlencode; class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface { @@ -71,10 +73,10 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface $replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier), ); $replacePlaceholdersInPath = compose( - $replacePlaceholders('\Functional\id'), - static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), // Fix duplicated bars + $replacePlaceholders(id(...)), + static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), ); - $replacePlaceholdersInQuery = $replacePlaceholders('\urlencode'); + $replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...)); return $redirectUri ->withPath($replacePlaceholdersInPath($redirectUri->getPath())) diff --git a/module/Core/src/Config/NotFoundRedirects.php b/module/Core/src/Config/NotFoundRedirects.php index 492a00bc..48437924 100644 --- a/module/Core/src/Config/NotFoundRedirects.php +++ b/module/Core/src/Config/NotFoundRedirects.php @@ -9,9 +9,9 @@ use JsonSerializable; final class NotFoundRedirects implements JsonSerializable { private function __construct( - private ?string $baseUrlRedirect, - private ?string $regular404Redirect, - private ?string $invalidShortUrlRedirect, + public readonly ?string $baseUrlRedirect, + public readonly ?string $regular404Redirect, + public readonly ?string $invalidShortUrlRedirect, ) { } @@ -33,21 +33,6 @@ final class NotFoundRedirects implements JsonSerializable return new self($config->baseUrlRedirect(), $config->regular404Redirect(), $config->invalidShortUrlRedirect()); } - public function baseUrlRedirect(): ?string - { - return $this->baseUrlRedirect; - } - - public function regular404Redirect(): ?string - { - return $this->regular404Redirect; - } - - public function invalidShortUrlRedirect(): ?string - { - return $this->invalidShortUrlRedirect; - } - public function jsonSerialize(): array { return [ diff --git a/module/Core/src/Domain/Model/DomainItem.php b/module/Core/src/Domain/Model/DomainItem.php index 5547fe8d..cc968e95 100644 --- a/module/Core/src/Domain/Model/DomainItem.php +++ b/module/Core/src/Domain/Model/DomainItem.php @@ -12,9 +12,9 @@ use Shlinkio\Shlink\Core\Entity\Domain; final class DomainItem implements JsonSerializable { private function __construct( - private string $authority, - private NotFoundRedirectConfigInterface $notFoundRedirectConfig, - private bool $isDefault, + private readonly string $authority, + public readonly NotFoundRedirectConfigInterface $notFoundRedirectConfig, + public readonly bool $isDefault, ) { } @@ -23,9 +23,9 @@ final class DomainItem implements JsonSerializable return new self($domain->getAuthority(), $domain, false); } - public static function forDefaultDomain(string $authority, NotFoundRedirectConfigInterface $config): self + public static function forDefaultDomain(string $defaultDomain, NotFoundRedirectConfigInterface $config): self { - return new self($authority, $config, true); + return new self($defaultDomain, $config, true); } public function jsonSerialize(): array @@ -41,14 +41,4 @@ final class DomainItem implements JsonSerializable { return $this->authority; } - - public function isDefault(): bool - { - return $this->isDefault; - } - - public function notFoundRedirectConfig(): NotFoundRedirectConfigInterface - { - return $this->notFoundRedirectConfig; - } } diff --git a/module/Core/src/Domain/Repository/DomainRepository.php b/module/Core/src/Domain/Repository/DomainRepository.php index 0a99b3c6..60c32499 100644 --- a/module/Core/src/Domain/Repository/DomainRepository.php +++ b/module/Core/src/Domain/Repository/DomainRepository.php @@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Core\Domain\Repository; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; -use Happyr\DoctrineSpecification\Spec; use Shlinkio\Shlink\Core\Domain\Spec\IsDomain; use Shlinkio\Shlink\Core\Entity\Domain; use Shlinkio\Shlink\Core\Entity\ShortUrl; @@ -77,10 +76,9 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe // FIXME The $apiKey->spec() method cannot be used here, as it returns a single spec which assumes the // ShortUrl is the root entity. Here, the Domain is the root entity. // Think on a way to centralize the conditional behavior and make $apiKey->spec() more flexible. - yield from $apiKey?->mapRoles(fn (string $roleName, array $meta) => match ($roleName) { + yield from $apiKey?->mapRoles(fn (Role $role, array $meta) => match ($role) { Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))], Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)], - default => [null, Spec::andX()], }) ?? []; } } diff --git a/module/Core/src/Entity/Domain.php b/module/Core/src/Entity/Domain.php index 65ca8ce6..9c31bbe2 100644 --- a/module/Core/src/Entity/Domain.php +++ b/module/Core/src/Entity/Domain.php @@ -66,8 +66,8 @@ class Domain extends AbstractEntity implements JsonSerializable, NotFoundRedirec public function configureNotFoundRedirects(NotFoundRedirects $redirects): void { - $this->baseUrlRedirect = $redirects->baseUrlRedirect(); - $this->regular404Redirect = $redirects->regular404Redirect(); - $this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect(); + $this->baseUrlRedirect = $redirects->baseUrlRedirect; + $this->regular404Redirect = $redirects->regular404Redirect; + $this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect; } } diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index 9fff1509..28c4c446 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver; use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -174,7 +175,7 @@ class ShortUrl extends AbstractEntity { /** @var Selectable $visits */ $visits = $this->visits; - $criteria = Criteria::create()->where(Criteria::expr()->eq('type', Visit::TYPE_IMPORTED)) + $criteria = Criteria::create()->where(Criteria::expr()->eq('type', VisitType::IMPORTED)) ->orderBy(['id' => 'DESC']) ->setMaxResults(1); diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index c509bcc3..23a518ca 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -11,29 +11,24 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit; use function Shlinkio\Shlink\Core\isCrawler; class Visit extends AbstractEntity implements JsonSerializable { - public const TYPE_VALID_SHORT_URL = 'valid_short_url'; - public const TYPE_IMPORTED = 'imported'; - public const TYPE_INVALID_SHORT_URL = 'invalid_short_url'; - public const TYPE_BASE_URL = 'base_url'; - public const TYPE_REGULAR_404 = 'regular_404'; - private string $referer; private Chronos $date; private ?string $remoteAddr = null; private ?string $visitedUrl = null; private string $userAgent; - private string $type; + private VisitType $type; private ?ShortUrl $shortUrl; private ?VisitLocation $visitLocation = null; private bool $potentialBot; - private function __construct(?ShortUrl $shortUrl, string $type) + private function __construct(?ShortUrl $shortUrl, VisitType $type) { $this->shortUrl = $shortUrl; $this->date = Chronos::now(); @@ -42,7 +37,7 @@ class Visit extends AbstractEntity implements JsonSerializable public static function forValidShortUrl(ShortUrl $shortUrl, Visitor $visitor, bool $anonymize = true): self { - $instance = new self($shortUrl, self::TYPE_VALID_SHORT_URL); + $instance = new self($shortUrl, VisitType::VALID_SHORT_URL); $instance->hydrateFromVisitor($visitor, $anonymize); return $instance; @@ -50,7 +45,7 @@ class Visit extends AbstractEntity implements JsonSerializable public static function fromImport(ShortUrl $shortUrl, ImportedShlinkVisit $importedVisit): self { - $instance = new self($shortUrl, self::TYPE_IMPORTED); + $instance = new self($shortUrl, VisitType::IMPORTED); $instance->userAgent = $importedVisit->userAgent(); $instance->potentialBot = isCrawler($instance->userAgent); $instance->referer = $importedVisit->referer(); @@ -64,7 +59,7 @@ class Visit extends AbstractEntity implements JsonSerializable public static function forBasePath(Visitor $visitor, bool $anonymize = true): self { - $instance = new self(null, self::TYPE_BASE_URL); + $instance = new self(null, VisitType::BASE_URL); $instance->hydrateFromVisitor($visitor, $anonymize); return $instance; @@ -72,7 +67,7 @@ class Visit extends AbstractEntity implements JsonSerializable public static function forInvalidShortUrl(Visitor $visitor, bool $anonymize = true): self { - $instance = new self(null, self::TYPE_INVALID_SHORT_URL); + $instance = new self(null, VisitType::INVALID_SHORT_URL); $instance->hydrateFromVisitor($visitor, $anonymize); return $instance; @@ -80,7 +75,7 @@ class Visit extends AbstractEntity implements JsonSerializable public static function forRegularNotFound(Visitor $visitor, bool $anonymize = true): self { - $instance = new self(null, self::TYPE_REGULAR_404); + $instance = new self(null, VisitType::REGULAR_404); $instance->hydrateFromVisitor($visitor, $anonymize); return $instance; @@ -88,10 +83,10 @@ class Visit extends AbstractEntity implements JsonSerializable private function hydrateFromVisitor(Visitor $visitor, bool $anonymize = true): void { - $this->userAgent = $visitor->getUserAgent(); - $this->referer = $visitor->getReferer(); - $this->remoteAddr = $this->processAddress($anonymize, $visitor->getRemoteAddress()); - $this->visitedUrl = $visitor->getVisitedUrl(); + $this->userAgent = $visitor->userAgent; + $this->referer = $visitor->referer; + $this->remoteAddr = $this->processAddress($anonymize, $visitor->remoteAddress); + $this->visitedUrl = $visitor->visitedUrl; $this->potentialBot = $visitor->isPotentialBot(); } @@ -150,7 +145,7 @@ class Visit extends AbstractEntity implements JsonSerializable return $this->visitedUrl; } - public function type(): string + public function type(): VisitType { return $this->type; } @@ -159,11 +154,19 @@ class Visit extends AbstractEntity implements JsonSerializable * Needed only for ArrayCollections to be able to apply criteria filtering * @internal */ - public function getType(): string + public function getType(): VisitType { return $this->type(); } + /** + * @internal + */ + public function getDate(): Chronos + { + return $this->date; + } + public function jsonSerialize(): array { return [ @@ -174,12 +177,4 @@ class Visit extends AbstractEntity implements JsonSerializable 'potentialBot' => $this->potentialBot, ]; } - - /** - * @internal - */ - public function getDate(): Chronos - { - return $this->date; - } } diff --git a/module/Core/src/ErrorHandler/Model/NotFoundType.php b/module/Core/src/ErrorHandler/Model/NotFoundType.php index 39970dea..f95368cb 100644 --- a/module/Core/src/ErrorHandler/Model/NotFoundType.php +++ b/module/Core/src/ErrorHandler/Model/NotFoundType.php @@ -7,13 +7,13 @@ namespace Shlinkio\Shlink\Core\ErrorHandler\Model; use Mezzio\Router\RouteResult; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; -use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use function rtrim; class NotFoundType { - private function __construct(private string $type) + private function __construct(private readonly VisitType $type) { } @@ -24,10 +24,10 @@ class NotFoundType $isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath; $type = match (true) { - $isBaseUrl => Visit::TYPE_BASE_URL, - $routeResult->isFailure() => Visit::TYPE_REGULAR_404, - $routeResult->getMatchedRouteName() === RedirectAction::class => Visit::TYPE_INVALID_SHORT_URL, - default => self::class, + $isBaseUrl => VisitType::BASE_URL, + $routeResult->isFailure() => VisitType::REGULAR_404, + $routeResult->getMatchedRouteName() === RedirectAction::class => VisitType::INVALID_SHORT_URL, + default => VisitType::VALID_SHORT_URL, }; return new self($type); @@ -35,16 +35,16 @@ class NotFoundType public function isBaseUrl(): bool { - return $this->type === Visit::TYPE_BASE_URL; + return $this->type === VisitType::BASE_URL; } public function isRegularNotFound(): bool { - return $this->type === Visit::TYPE_REGULAR_404; + return $this->type === VisitType::REGULAR_404; } public function isInvalidShortUrl(): bool { - return $this->type === Visit::TYPE_INVALID_SHORT_URL; + return $this->type === VisitType::INVALID_SHORT_URL; } } diff --git a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php index c4bc1818..6fadaa5d 100644 --- a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php +++ b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php @@ -8,15 +8,10 @@ use JsonSerializable; abstract class AbstractVisitEvent implements JsonSerializable { - public function __construct(protected string $visitId) + public function __construct(public readonly string $visitId) { } - public function visitId(): string - { - return $this->visitId; - } - public function jsonSerialize(): array { return ['visitId' => $this->visitId]; diff --git a/module/Core/src/EventDispatcher/Event/UrlVisited.php b/module/Core/src/EventDispatcher/Event/UrlVisited.php index 633b439e..02452a3e 100644 --- a/module/Core/src/EventDispatcher/Event/UrlVisited.php +++ b/module/Core/src/EventDispatcher/Event/UrlVisited.php @@ -6,13 +6,8 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event; final class UrlVisited extends AbstractVisitEvent { - public function __construct(string $visitId, private ?string $originalIpAddress = null) + public function __construct(string $visitId, public readonly ?string $originalIpAddress = null) { parent::__construct($visitId); } - - public function originalIpAddress(): ?string - { - return $this->originalIpAddress; - } } diff --git a/module/Core/src/EventDispatcher/LocateVisit.php b/module/Core/src/EventDispatcher/LocateVisit.php index fbd32962..197ce9a0 100644 --- a/module/Core/src/EventDispatcher/LocateVisit.php +++ b/module/Core/src/EventDispatcher/LocateVisit.php @@ -30,7 +30,7 @@ class LocateVisit public function __invoke(UrlVisited $shortUrlVisited): void { - $visitId = $shortUrlVisited->visitId(); + $visitId = $shortUrlVisited->visitId; /** @var Visit|null $visit */ $visit = $this->em->find(Visit::class, $visitId); @@ -41,7 +41,7 @@ class LocateVisit return; } - $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit); + $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit); $this->eventDispatcher->dispatch(new VisitLocated($visitId)); } diff --git a/module/Core/src/EventDispatcher/NotifyVisitToMercure.php b/module/Core/src/EventDispatcher/NotifyVisitToMercure.php index ed205f40..096e7e65 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToMercure.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToMercure.php @@ -27,7 +27,7 @@ class NotifyVisitToMercure public function __invoke(VisitLocated $shortUrlLocated): void { - $visitId = $shortUrlLocated->visitId(); + $visitId = $shortUrlLocated->visitId; /** @var Visit|null $visit */ $visit = $this->em->find(Visit::class, $visitId); diff --git a/module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php b/module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php index f05ecf64..a81f2cab 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php @@ -37,7 +37,7 @@ class NotifyVisitToRabbitMq return; } - $visitId = $shortUrlLocated->visitId(); + $visitId = $shortUrlLocated->visitId; $visit = $this->em->find(Visit::class, $visitId); if ($visit === null) { diff --git a/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php b/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php index 73ff9266..1bf09517 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php @@ -40,7 +40,7 @@ class NotifyVisitToWebHooks return; } - $visitId = $shortUrlLocated->visitId(); + $visitId = $shortUrlLocated->visitId; /** @var Visit|null $visit */ $visit = $this->em->find(Visit::class, $visitId); diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index e6f3bd0d..0d331400 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -20,8 +20,8 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self { - $shortCode = $identifier->shortCode(); - $domain = $identifier->domain(); + $shortCode = $identifier->shortCode; + $domain = $identifier->domain; $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $e = new self(sprintf( 'Impossible to delete short URL with short code "%s"%s, since it has more than "%s" visits.', diff --git a/module/Core/src/Exception/ShortUrlNotFoundException.php b/module/Core/src/Exception/ShortUrlNotFoundException.php index 0ae29da5..c59c20ef 100644 --- a/module/Core/src/Exception/ShortUrlNotFoundException.php +++ b/module/Core/src/Exception/ShortUrlNotFoundException.php @@ -20,8 +20,8 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail public static function fromNotFound(ShortUrlIdentifier $identifier): self { - $shortCode = $identifier->shortCode(); - $domain = $identifier->domain(); + $shortCode = $identifier->shortCode; + $domain = $identifier->domain; $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix)); diff --git a/module/Core/src/Importer/ShortUrlImporting.php b/module/Core/src/Importer/ShortUrlImporting.php index e1680517..4aa87166 100644 --- a/module/Core/src/Importer/ShortUrlImporting.php +++ b/module/Core/src/Importer/ShortUrlImporting.php @@ -14,7 +14,7 @@ use function sprintf; final class ShortUrlImporting { - private function __construct(private ShortUrl $shortUrl, private bool $isNew) + private function __construct(private readonly ShortUrl $shortUrl, private readonly bool $isNew) { } diff --git a/module/Core/src/Model/AbstractInfinitePaginableListParams.php b/module/Core/src/Model/AbstractInfinitePaginableListParams.php index ae107fdc..d4b2aaab 100644 --- a/module/Core/src/Model/AbstractInfinitePaginableListParams.php +++ b/module/Core/src/Model/AbstractInfinitePaginableListParams.php @@ -10,8 +10,8 @@ abstract class AbstractInfinitePaginableListParams { private const FIRST_PAGE = 1; - private int $page; - private int $itemsPerPage; + public readonly int $page; + public readonly int $itemsPerPage; protected function __construct(?int $page, ?int $itemsPerPage) { @@ -28,14 +28,4 @@ abstract class AbstractInfinitePaginableListParams { return $itemsPerPage === null || $itemsPerPage < 0 ? Paginator::ALL_ITEMS : $itemsPerPage; } - - public function getPage(): int - { - return $this->page; - } - - public function getItemsPerPage(): int - { - return $this->itemsPerPage; - } } diff --git a/module/Core/src/Model/Ordering.php b/module/Core/src/Model/Ordering.php index bd648227..5adbb161 100644 --- a/module/Core/src/Model/Ordering.php +++ b/module/Core/src/Model/Ordering.php @@ -8,7 +8,7 @@ final class Ordering { private const DEFAULT_DIR = 'ASC'; - private function __construct(private ?string $field, private string $dir) + private function __construct(public readonly ?string $field, public readonly string $direction) { } @@ -26,16 +26,6 @@ final class Ordering return self::fromTuple([null, null]); } - public function orderField(): ?string - { - return $this->field; - } - - public function orderDirection(): string - { - return $this->dir; - } - public function hasOrderField(): bool { return $this->field !== null; diff --git a/module/Core/src/Model/ShortUrlIdentifier.php b/module/Core/src/Model/ShortUrlIdentifier.php index 815a5313..d2d6cbbc 100644 --- a/module/Core/src/Model/ShortUrlIdentifier.php +++ b/module/Core/src/Model/ShortUrlIdentifier.php @@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputInterface; final class ShortUrlIdentifier { - public function __construct(private string $shortCode, private ?string $domain = null) + private function __construct(public readonly string $shortCode, public readonly ?string $domain = null) { } @@ -54,14 +54,4 @@ final class ShortUrlIdentifier { return new self($shortCode, $domain); } - - public function shortCode(): string - { - return $this->shortCode; - } - - public function domain(): ?string - { - return $this->domain; - } } diff --git a/module/Core/src/Model/ShortUrlsParams.php b/module/Core/src/Model/ShortUrlsParams.php index 95cf4df6..bd6dc556 100644 --- a/module/Core/src/Model/ShortUrlsParams.php +++ b/module/Core/src/Model/ShortUrlsParams.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Exception\ValidationException; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter; use function Shlinkio\Shlink\Common\buildDateRange; @@ -15,15 +16,12 @@ final class ShortUrlsParams { public const ORDERABLE_FIELDS = ['longUrl', 'shortCode', 'dateCreated', 'title', 'visits']; public const DEFAULT_ITEMS_PER_PAGE = 10; - public const TAGS_MODE_ANY = 'any'; - public const TAGS_MODE_ALL = 'all'; private int $page; private int $itemsPerPage; private ?string $searchTerm; private array $tags; - /** @var self::TAGS_MODE_ANY|self::TAGS_MODE_ALL */ - private string $tagsMode = self::TAGS_MODE_ANY; + private TagsMode $tagsMode = TagsMode::ANY; private Ordering $orderBy; private ?DateRange $dateRange; @@ -68,7 +66,16 @@ final class ShortUrlsParams $this->itemsPerPage = (int) ( $inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE ); - $this->tagsMode = $inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE) ?? self::TAGS_MODE_ANY; + $this->tagsMode = $this->resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE)); + } + + private function resolveTagsMode(?string $rawTagsMode): TagsMode + { + if ($rawTagsMode === null) { + return TagsMode::ANY; + } + + return TagsMode::tryFrom($rawTagsMode) ?? TagsMode::ANY; } public function page(): int @@ -101,10 +108,7 @@ final class ShortUrlsParams return $this->dateRange; } - /** - * @return self::TAGS_MODE_ANY|self::TAGS_MODE_ALL - */ - public function tagsMode(): string + public function tagsMode(): TagsMode { return $this->tagsMode; } diff --git a/module/Core/src/Model/Visitor.php b/module/Core/src/Model/Visitor.php index 9436e900..2207fad8 100644 --- a/module/Core/src/Model/Visitor.php +++ b/module/Core/src/Model/Visitor.php @@ -18,10 +18,10 @@ final class Visitor public const REMOTE_ADDRESS_MAX_LENGTH = 256; public const VISITED_URL_MAX_LENGTH = 2048; - private string $userAgent; - private string $referer; - private string $visitedUrl; - private ?string $remoteAddress; + public readonly string $userAgent; + public readonly string $referer; + public readonly string $visitedUrl; + public readonly ?string $remoteAddress; private bool $potentialBot; public function __construct(string $userAgent, string $referer, ?string $remoteAddress, string $visitedUrl) @@ -61,26 +61,6 @@ final class Visitor return new self('cf-facebook', '', null, ''); } - public function getUserAgent(): string - { - return $this->userAgent; - } - - public function getReferer(): string - { - return $this->referer; - } - - public function getRemoteAddress(): ?string - { - return $this->remoteAddress; - } - - public function getVisitedUrl(): string - { - return $this->visitedUrl; - } - public function isPotentialBot(): bool { return $this->potentialBot; diff --git a/module/Core/src/Model/VisitsParams.php b/module/Core/src/Model/VisitsParams.php index 718a4bc5..ab9e8002 100644 --- a/module/Core/src/Model/VisitsParams.php +++ b/module/Core/src/Model/VisitsParams.php @@ -10,13 +10,13 @@ use function Shlinkio\Shlink\Core\parseDateRangeFromQuery; final class VisitsParams extends AbstractInfinitePaginableListParams { - private DateRange $dateRange; + public readonly DateRange $dateRange; public function __construct( ?DateRange $dateRange = null, ?int $page = null, ?int $itemsPerPage = null, - private bool $excludeBots = false, + public readonly bool $excludeBots = false, ) { parent::__construct($page, $itemsPerPage); $this->dateRange = $dateRange ?? DateRange::emptyInstance(); @@ -31,14 +31,4 @@ final class VisitsParams extends AbstractInfinitePaginableListParams isset($query['excludeBots']), ); } - - public function getDateRange(): DateRange - { - return $this->dateRange; - } - - public function excludeBots(): bool - { - return $this->excludeBots; - } } diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index 9852c530..7a3def16 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -15,7 +15,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Model\Ordering; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; -use Shlinkio\Shlink\Core\Model\ShortUrlsParams; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; @@ -47,8 +47,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU private function processOrderByForList(QueryBuilder $qb, Ordering $orderBy): array { - $fieldName = $orderBy->orderField(); - $order = $orderBy->orderDirection(); + $fieldName = $orderBy->field; + $order = $orderBy->direction; if ($fieldName === 'visits') { // FIXME This query is inefficient. @@ -116,8 +116,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU // Filter by tags if provided if (! empty($tags)) { - $tagsMode = $filtering->tagsMode() ?? ShortUrlsParams::TAGS_MODE_ANY; - $tagsMode === ShortUrlsParams::TAGS_MODE_ANY + $tagsMode = $filtering->tagsMode() ?? TagsMode::ANY; + $tagsMode === TagsMode::ANY ? $qb->join('s.tags', 't')->andWhere($qb->expr()->in('t.name', $tags)) : $this->joinAllTags($qb, $tags); } @@ -146,8 +146,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU $query = $this->getEntityManager()->createQuery($dql); $query->setMaxResults(1) ->setParameters([ - 'shortCode' => $identifier->shortCode(), - 'domain' => $identifier->domain(), + 'shortCode' => $identifier->shortCode, + 'domain' => $identifier->domain, ]); // Since we ordered by domain, we will have first the URL matching provided domain, followed by the one @@ -198,10 +198,10 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU $qb->from(ShortUrl::class, 's') ->where($qb->expr()->isNotNull('s.shortCode')) ->andWhere($qb->expr()->eq('s.shortCode', ':slug')) - ->setParameter('slug', $identifier->shortCode()) + ->setParameter('slug', $identifier->shortCode) ->setMaxResults(1); - $this->whereDomainIs($qb, $identifier->domain()); + $this->whereDomainIs($qb, $identifier->domain); $this->applySpecification($qb, $spec, 's'); diff --git a/module/Core/src/Repository/TagRepository.php b/module/Core/src/Repository/TagRepository.php index aa24e0a1..2a144b38 100644 --- a/module/Core/src/Repository/TagRepository.php +++ b/module/Core/src/Repository/TagRepository.php @@ -41,8 +41,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito */ public function findTagsWithInfo(?TagsListFiltering $filtering = null): array { - $orderField = $filtering?->orderBy()?->orderField(); - $orderDir = $filtering?->orderBy()?->orderDirection(); + $orderField = $filtering?->orderBy?->field; + $orderDir = $filtering?->orderBy?->direction; $orderMainQuery = contains(['shortUrlsCount', 'visitsCount'], $orderField); $conn = $this->getEntityManager()->getConnection(); @@ -51,16 +51,16 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito if (! $orderMainQuery) { $subQb->orderBy('t.name', $orderDir ?? 'ASC') - ->setMaxResults($filtering?->limit() ?? PHP_INT_MAX) - ->setFirstResult($filtering?->offset() ?? 0); + ->setMaxResults($filtering?->limit ?? PHP_INT_MAX) + ->setFirstResult($filtering?->offset ?? 0); } - $searchTerm = $filtering?->searchTerm(); + $searchTerm = $filtering?->searchTerm; if ($searchTerm !== null) { $subQb->andWhere($subQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%'))); } - $apiKey = $filtering?->apiKey(); + $apiKey = $filtering?->apiKey; $this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't'); // A native query builder needs to be used here, because DQL and ORM query builders do not support @@ -81,14 +81,13 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito ->groupBy('t.id_0', 't.name_1'); // Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates - $apiKey?->mapRoles(static fn (string $roleName, array $meta) => match ($roleName) { + $apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) { Role::DOMAIN_SPECIFIC => $nativeQb->andWhere( $nativeQb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))), ), Role::AUTHORED_SHORT_URLS => $nativeQb->andWhere( $nativeQb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())), ), - default => $nativeQb, }); if ($orderMainQuery) { @@ -97,8 +96,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito $orderField === 'shortUrlsCount' ? 'short_urls_count' : 'visits_count', $orderDir ?? 'ASC', ) - ->setMaxResults($filtering?->limit() ?? PHP_INT_MAX) - ->setFirstResult($filtering?->offset() ?? 0); + ->setMaxResults($filtering?->limit ?? PHP_INT_MAX) + ->setFirstResult($filtering?->offset ?? 0); } // Add ordering by tag name, as a fallback in case of same amount, or as default ordering diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php index 51a0c333..e0b51ce9 100644 --- a/module/Core/src/Repository/VisitRepository.php +++ b/module/Core/src/Repository/VisitRepository.php @@ -86,7 +86,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo public function findVisitsByShortCode(ShortUrlIdentifier $identifier, VisitsListFiltering $filtering): array { $qb = $this->createVisitsByShortCodeQueryBuilder($identifier, $filtering); - return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset()); + return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset); } public function countVisitsByShortCode(ShortUrlIdentifier $identifier, VisitsCountFiltering $filtering): int @@ -103,7 +103,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo ): QueryBuilder { /** @var ShortUrlRepositoryInterface $shortUrlRepo */ $shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class); - $shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey()?->spec())?->getId() ?? '-1'; + $shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey?->spec())?->getId() ?? '-1'; // Parameters in this query need to be part of the query itself, as we need to use it as sub-query later // Since they are not provided by the caller, it's reasonably safe @@ -111,12 +111,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo $qb->from(Visit::class, 'v') ->where($qb->expr()->eq('v.shortUrl', $shortUrlId)); - if ($filtering->excludeBots()) { + if ($filtering->excludeBots) { $qb->andWhere($qb->expr()->eq('v.potentialBot', 'false')); } // Apply date range filtering - $this->applyDatesInline($qb, $filtering->dateRange()); + $this->applyDatesInline($qb, $filtering->dateRange); return $qb; } @@ -124,7 +124,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo public function findVisitsByTag(string $tag, VisitsListFiltering $filtering): array { $qb = $this->createVisitsByTagQueryBuilder($tag, $filtering); - return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset()); + return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset); } public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int @@ -144,12 +144,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo ->join('s.tags', 't') ->where($qb->expr()->eq('t.name', $this->getEntityManager()->getConnection()->quote($tag))); - if ($filtering->excludeBots()) { + if ($filtering->excludeBots) { $qb->andWhere($qb->expr()->eq('v.potentialBot', 'false')); } - $this->applyDatesInline($qb, $filtering->dateRange()); - $this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v'); + $this->applyDatesInline($qb, $filtering->dateRange); + $this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v'); return $qb; } @@ -160,7 +160,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array { $qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering); - return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset()); + return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset); } public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int @@ -185,12 +185,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo ->where($qb->expr()->eq('d.authority', $this->getEntityManager()->getConnection()->quote($domain))); } - if ($filtering->excludeBots()) { + if ($filtering->excludeBots) { $qb->andWhere($qb->expr()->eq('v.potentialBot', 'false')); } - $this->applyDatesInline($qb, $filtering->dateRange()); - $this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v'); + $this->applyDatesInline($qb, $filtering->dateRange); + $this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v'); return $qb; } @@ -199,7 +199,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo { $qb = $this->createAllVisitsQueryBuilder($filtering); $qb->andWhere($qb->expr()->isNull('v.shortUrl')); - return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset()); + return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset); } public function countOrphanVisits(VisitsCountFiltering $filtering): int @@ -215,9 +215,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo $qb = $this->createAllVisitsQueryBuilder($filtering); $qb->andWhere($qb->expr()->isNotNull('v.shortUrl')); - $this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec()); + $this->applySpecification($qb, $filtering->apiKey?->inlinedSpec()); - return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset()); + return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset); } public function countNonOrphanVisits(VisitsCountFiltering $filtering): int @@ -232,11 +232,11 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo $qb = $this->getEntityManager()->createQueryBuilder(); $qb->from(Visit::class, 'v'); - if ($filtering->excludeBots()) { + if ($filtering->excludeBots) { $qb->andWhere($qb->expr()->eq('v.potentialBot', 'false')); } - $this->applyDatesInline($qb, $filtering->dateRange()); + $this->applyDatesInline($qb, $filtering->dateRange); return $qb; } diff --git a/module/Core/src/ShortUrl/Model/TagsMode.php b/module/Core/src/ShortUrl/Model/TagsMode.php new file mode 100644 index 00000000..5ae2d9bb --- /dev/null +++ b/module/Core/src/ShortUrl/Model/TagsMode.php @@ -0,0 +1,13 @@ +tags; } - public function tagsMode(): ?string + public function tagsMode(): ?TagsMode { return $this->tagsMode; } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php index 089915e3..04645126 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Persistence; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Model\Ordering; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Rest\Entity\ApiKey; class ShortUrlsListFiltering extends ShortUrlsCountFiltering @@ -17,7 +18,7 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering private Ordering $orderBy, ?string $searchTerm = null, array $tags = [], - ?string $tagsMode = null, + ?TagsMode $tagsMode = null, ?DateRange $dateRange = null, ?ApiKey $apiKey = null, ) { diff --git a/module/Core/src/Tag/Model/TagInfo.php b/module/Core/src/Tag/Model/TagInfo.php index 6e917399..8a4f196b 100644 --- a/module/Core/src/Tag/Model/TagInfo.php +++ b/module/Core/src/Tag/Model/TagInfo.php @@ -8,23 +8,11 @@ use JsonSerializable; final class TagInfo implements JsonSerializable { - public function __construct(private string $tag, private int $shortUrlsCount, private int $visitsCount) - { - } - - public function tag(): string - { - return $this->tag; - } - - public function shortUrlsCount(): int - { - return $this->shortUrlsCount; - } - - public function visitsCount(): int - { - return $this->visitsCount; + public function __construct( + public readonly string $tag, + public readonly int $shortUrlsCount, + public readonly int $visitsCount, + ) { } public function jsonSerialize(): array diff --git a/module/Core/src/Tag/Model/TagRenaming.php b/module/Core/src/Tag/Model/TagRenaming.php index 3bdae21c..9c523b8b 100644 --- a/module/Core/src/Tag/Model/TagRenaming.php +++ b/module/Core/src/Tag/Model/TagRenaming.php @@ -10,7 +10,7 @@ use function sprintf; final class TagRenaming { - private function __construct(private string $oldName, private string $newName) + private function __construct(public readonly string $oldName, public readonly string $newName) { } @@ -31,16 +31,6 @@ final class TagRenaming return self::fromNames($payload['oldName'], $payload['newName']); } - public function oldName(): string - { - return $this->oldName; - } - - public function newName(): string - { - return $this->newName; - } - public function nameChanged(): bool { return $this->oldName !== $this->newName; diff --git a/module/Core/src/Tag/Model/TagsListFiltering.php b/module/Core/src/Tag/Model/TagsListFiltering.php index 8f078788..236dde4a 100644 --- a/module/Core/src/Tag/Model/TagsListFiltering.php +++ b/module/Core/src/Tag/Model/TagsListFiltering.php @@ -10,41 +10,16 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; final class TagsListFiltering { public function __construct( - private ?int $limit = null, - private ?int $offset = null, - private ?string $searchTerm = null, - private ?Ordering $orderBy = null, - private ?ApiKey $apiKey = null, + public readonly ?int $limit = null, + public readonly ?int $offset = null, + public readonly ?string $searchTerm = null, + public readonly ?Ordering $orderBy = null, + public readonly ?ApiKey $apiKey = null, ) { } public static function fromRangeAndParams(int $limit, int $offset, TagsParams $params, ?ApiKey $apiKey): self { - return new self($limit, $offset, $params->searchTerm(), $params->orderBy(), $apiKey); - } - - public function limit(): ?int - { - return $this->limit; - } - - public function offset(): ?int - { - return $this->offset; - } - - public function searchTerm(): ?string - { - return $this->searchTerm; - } - - public function orderBy(): ?Ordering - { - return $this->orderBy; - } - - public function apiKey(): ?ApiKey - { - return $this->apiKey; + return new self($limit, $offset, $params->searchTerm, $params->orderBy, $apiKey); } } diff --git a/module/Core/src/Tag/Model/TagsParams.php b/module/Core/src/Tag/Model/TagsParams.php index 3f40debe..633fd5f2 100644 --- a/module/Core/src/Tag/Model/TagsParams.php +++ b/module/Core/src/Tag/Model/TagsParams.php @@ -12,9 +12,9 @@ use function Shlinkio\Shlink\Common\parseOrderBy; final class TagsParams extends AbstractInfinitePaginableListParams { private function __construct( - private ?string $searchTerm, - private Ordering $orderBy, - private bool $withStats, + public readonly ?string $searchTerm, + public readonly Ordering $orderBy, + public readonly bool $withStats, ?int $page, ?int $itemsPerPage, ) { @@ -31,19 +31,4 @@ final class TagsParams extends AbstractInfinitePaginableListParams isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null, ); } - - public function searchTerm(): ?string - { - return $this->searchTerm; - } - - public function orderBy(): Ordering - { - return $this->orderBy; - } - - public function withStats(): bool - { - return $this->withStats; - } } diff --git a/module/Core/src/Tag/Paginator/Adapter/AbstractTagsPaginatorAdapter.php b/module/Core/src/Tag/Paginator/Adapter/AbstractTagsPaginatorAdapter.php index ba6bc78d..ee0086cd 100644 --- a/module/Core/src/Tag/Paginator/Adapter/AbstractTagsPaginatorAdapter.php +++ b/module/Core/src/Tag/Paginator/Adapter/AbstractTagsPaginatorAdapter.php @@ -30,7 +30,7 @@ abstract class AbstractTagsPaginatorAdapter implements AdapterInterface new WithApiKeySpecsEnsuringJoin($this->apiKey), ]; - $searchTerm = $this->params->searchTerm(); + $searchTerm = $this->params->searchTerm; if ($searchTerm !== null) { $conditions[] = Spec::like('name', $searchTerm); } diff --git a/module/Core/src/Tag/Paginator/Adapter/TagsPaginatorAdapter.php b/module/Core/src/Tag/Paginator/Adapter/TagsPaginatorAdapter.php index d6bc0b7b..7d54940e 100644 --- a/module/Core/src/Tag/Paginator/Adapter/TagsPaginatorAdapter.php +++ b/module/Core/src/Tag/Paginator/Adapter/TagsPaginatorAdapter.php @@ -15,13 +15,13 @@ class TagsPaginatorAdapter extends AbstractTagsPaginatorAdapter new WithApiKeySpecsEnsuringJoin($this->apiKey), Spec::orderBy( 'name', // Ordering by other fields makes no sense here - $this->params->orderBy()->orderDirection(), + $this->params->orderBy->direction, ), Spec::limit($length), Spec::offset($offset), ]; - $searchTerm = $this->params->searchTerm(); + $searchTerm = $this->params->searchTerm; if ($searchTerm !== null) { $conditions[] = Spec::like('name', $searchTerm); } diff --git a/module/Core/src/Tag/TagService.php b/module/Core/src/Tag/TagService.php index 40eb413f..b8d7f710 100644 --- a/module/Core/src/Tag/TagService.php +++ b/module/Core/src/Tag/TagService.php @@ -49,8 +49,8 @@ class TagService implements TagServiceInterface private function createPaginator(AdapterInterface $adapter, TagsParams $params): Paginator { return (new Paginator($adapter)) - ->setMaxPerPage($params->getItemsPerPage()) - ->setCurrentPage($params->getPage()); + ->setMaxPerPage($params->itemsPerPage) + ->setCurrentPage($params->page); } /** @@ -83,17 +83,17 @@ class TagService implements TagServiceInterface $repo = $this->em->getRepository(Tag::class); /** @var Tag|null $tag */ - $tag = $repo->findOneBy(['name' => $renaming->oldName()]); + $tag = $repo->findOneBy(['name' => $renaming->oldName]); if ($tag === null) { - throw TagNotFoundException::fromTag($renaming->oldName()); + throw TagNotFoundException::fromTag($renaming->oldName); } - $newNameExists = $renaming->nameChanged() && $repo->count(['name' => $renaming->newName()]) > 0; + $newNameExists = $renaming->nameChanged() && $repo->count(['name' => $renaming->newName]) > 0; if ($newNameExists) { throw TagConflictException::forExistingTag($renaming); } - $tag->rename($renaming->newName()); + $tag->rename($renaming->newName); $this->em->flush(); return $tag; diff --git a/module/Core/src/Validation/ShortUrlsParamsInputFilter.php b/module/Core/src/Validation/ShortUrlsParamsInputFilter.php index 6c0443aa..50953310 100644 --- a/module/Core/src/Validation/ShortUrlsParamsInputFilter.php +++ b/module/Core/src/Validation/ShortUrlsParamsInputFilter.php @@ -9,6 +9,7 @@ use Laminas\Validator\InArray; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Validation; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; class ShortUrlsParamsInputFilter extends InputFilter { @@ -43,7 +44,7 @@ class ShortUrlsParamsInputFilter extends InputFilter $tagsMode = $this->createInput(self::TAGS_MODE, false); $tagsMode->getValidatorChain()->attach(new InArray([ - 'haystack' => [ShortUrlsParams::TAGS_MODE_ALL, ShortUrlsParams::TAGS_MODE_ANY], + 'haystack' => [TagsMode::ALL->value, TagsMode::ANY->value], 'strict' => InArray::COMPARE_STRICT, ])); $this->add($tagsMode); diff --git a/module/Core/src/Visit/Model/VisitType.php b/module/Core/src/Visit/Model/VisitType.php new file mode 100644 index 00000000..0c4b8841 --- /dev/null +++ b/module/Core/src/Visit/Model/VisitType.php @@ -0,0 +1,16 @@ +visitRepository->countVisitsByDomain( $this->domain, new VisitsCountFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, ), ); @@ -38,8 +38,8 @@ class DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte return $this->visitRepository->findVisitsByDomain( $this->domain, new VisitsListFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, $length, $offset, diff --git a/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php index ba5b6663..5f06ea09 100644 --- a/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php +++ b/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php @@ -23,8 +23,8 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda protected function doCount(): int { return $this->repo->countNonOrphanVisits(new VisitsCountFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, )); } @@ -32,8 +32,8 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda public function getSlice(int $offset, int $length): iterable { return $this->repo->findNonOrphanVisits(new VisitsListFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, $length, $offset, diff --git a/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php index 8a47c9d7..f18dbb05 100644 --- a/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php +++ b/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php @@ -19,16 +19,16 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte protected function doCount(): int { return $this->repo->countOrphanVisits(new VisitsCountFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, )); } public function getSlice(int $offset, int $length): iterable { return $this->repo->findOrphanVisits(new VisitsListFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, null, $length, $offset, diff --git a/module/Core/src/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapter.php index 2e47fbf8..5169c327 100644 --- a/module/Core/src/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapter.php +++ b/module/Core/src/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapter.php @@ -27,8 +27,8 @@ class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdap return $this->visitRepository->findVisitsByShortCode( $this->identifier, new VisitsListFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, $length, $offset, @@ -41,8 +41,8 @@ class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdap return $this->visitRepository->countVisitsByShortCode( $this->identifier, new VisitsCountFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, ), ); diff --git a/module/Core/src/Visit/Paginator/Adapter/TagVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/TagVisitsPaginatorAdapter.php index 162b6cba..aed79d02 100644 --- a/module/Core/src/Visit/Paginator/Adapter/TagVisitsPaginatorAdapter.php +++ b/module/Core/src/Visit/Paginator/Adapter/TagVisitsPaginatorAdapter.php @@ -26,8 +26,8 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter return $this->visitRepository->findVisitsByTag( $this->tag, new VisitsListFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, $length, $offset, @@ -40,8 +40,8 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter return $this->visitRepository->countVisitsByTag( $this->tag, new VisitsCountFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, ), ); diff --git a/module/Core/src/Visit/Persistence/VisitsCountFiltering.php b/module/Core/src/Visit/Persistence/VisitsCountFiltering.php index 140ec9b9..f839a945 100644 --- a/module/Core/src/Visit/Persistence/VisitsCountFiltering.php +++ b/module/Core/src/Visit/Persistence/VisitsCountFiltering.php @@ -10,9 +10,9 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; class VisitsCountFiltering { public function __construct( - private ?DateRange $dateRange = null, - private bool $excludeBots = false, - private ?ApiKey $apiKey = null, + public readonly ?DateRange $dateRange = null, + public readonly bool $excludeBots = false, + public readonly ?ApiKey $apiKey = null, ) { } @@ -20,19 +20,4 @@ class VisitsCountFiltering { return new self(null, false, $apiKey); } - - public function dateRange(): ?DateRange - { - return $this->dateRange; - } - - public function excludeBots(): bool - { - return $this->excludeBots; - } - - public function apiKey(): ?ApiKey - { - return $this->apiKey; - } } diff --git a/module/Core/src/Visit/Persistence/VisitsListFiltering.php b/module/Core/src/Visit/Persistence/VisitsListFiltering.php index b17964a6..747a3ce0 100644 --- a/module/Core/src/Visit/Persistence/VisitsListFiltering.php +++ b/module/Core/src/Visit/Persistence/VisitsListFiltering.php @@ -13,19 +13,9 @@ final class VisitsListFiltering extends VisitsCountFiltering ?DateRange $dateRange = null, bool $excludeBots = false, ?ApiKey $apiKey = null, - private ?int $limit = null, - private ?int $offset = null, + public readonly ?int $limit = null, + public readonly ?int $offset = null, ) { parent::__construct($dateRange, $excludeBots, $apiKey); } - - public function limit(): ?int - { - return $this->limit; - } - - public function offset(): ?int - { - return $this->offset; - } } diff --git a/module/Core/src/Visit/Spec/CountOfNonOrphanVisits.php b/module/Core/src/Visit/Spec/CountOfNonOrphanVisits.php index 52be52a8..a7b2a1d6 100644 --- a/module/Core/src/Visit/Spec/CountOfNonOrphanVisits.php +++ b/module/Core/src/Visit/Spec/CountOfNonOrphanVisits.php @@ -22,14 +22,14 @@ class CountOfNonOrphanVisits extends BaseSpecification { $conditions = [ Spec::isNotNull('shortUrl'), - new InDateRange($this->filtering->dateRange()), + new InDateRange($this->filtering->dateRange), ]; - if ($this->filtering->excludeBots()) { + if ($this->filtering->excludeBots) { $conditions[] = Spec::eq('potentialBot', false); } - $apiKey = $this->filtering->apiKey(); + $apiKey = $this->filtering->apiKey; if ($apiKey !== null) { $conditions[] = new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrl'); } diff --git a/module/Core/src/Visit/Spec/CountOfOrphanVisits.php b/module/Core/src/Visit/Spec/CountOfOrphanVisits.php index d8e6b2d2..106350c6 100644 --- a/module/Core/src/Visit/Spec/CountOfOrphanVisits.php +++ b/module/Core/src/Visit/Spec/CountOfOrphanVisits.php @@ -21,10 +21,10 @@ class CountOfOrphanVisits extends BaseSpecification { $conditions = [ Spec::isNull('shortUrl'), - new InDateRange($this->filtering->dateRange()), + new InDateRange($this->filtering->dateRange), ]; - if ($this->filtering->excludeBots()) { + if ($this->filtering->excludeBots) { $conditions[] = Spec::eq('potentialBot', false); } diff --git a/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php b/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php index 9f4842f5..c9d30b8d 100644 --- a/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php +++ b/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php @@ -17,7 +17,7 @@ class OrphanVisitDataTransformer implements DataTransformerInterface { $serializedVisit = $visit->jsonSerialize(); $serializedVisit['visitedUrl'] = $visit->visitedUrl(); - $serializedVisit['type'] = $visit->type(); + $serializedVisit['type'] = $visit->type()->value; return $serializedVisit; } diff --git a/module/Core/src/Visit/VisitsStatsHelper.php b/module/Core/src/Visit/VisitsStatsHelper.php index 007ed334..4f19103f 100644 --- a/module/Core/src/Visit/VisitsStatsHelper.php +++ b/module/Core/src/Visit/VisitsStatsHelper.php @@ -129,8 +129,8 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator { $paginator = new Paginator($adapter); - $paginator->setMaxPerPage($params->getItemsPerPage()) - ->setCurrentPage($params->getPage()); + $paginator->setMaxPerPage($params->itemsPerPage) + ->setCurrentPage($params->page); return $paginator; } diff --git a/module/Core/src/Visit/VisitsTracker.php b/module/Core/src/Visit/VisitsTracker.php index f4e5bf92..3aef46df 100644 --- a/module/Core/src/Visit/VisitsTracker.php +++ b/module/Core/src/Visit/VisitsTracker.php @@ -72,6 +72,6 @@ class VisitsTracker implements VisitsTrackerInterface $this->em->persist($visit); $this->em->flush(); - $this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->getRemoteAddress())); + $this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress)); } } diff --git a/module/Core/test-db/Repository/ShortUrlRepositoryTest.php b/module/Core/test-db/Repository/ShortUrlRepositoryTest.php index 4ad89629..05c65ee3 100644 --- a/module/Core/test-db/Repository/ShortUrlRepositoryTest.php +++ b/module/Core/test-db/Repository/ShortUrlRepositoryTest.php @@ -14,9 +14,9 @@ use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\Ordering; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; -use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering; use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver; @@ -227,7 +227,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase Ordering::emptyInstance(), null, ['foo', 'bar'], - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY, ))); self::assertCount(1, $this->repo->findList(new ShortUrlsListFiltering( null, @@ -235,15 +235,11 @@ class ShortUrlRepositoryTest extends DatabaseTestCase Ordering::emptyInstance(), null, ['foo', 'bar'], - ShortUrlsParams::TAGS_MODE_ALL, + TagsMode::ALL, ))); self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar']))); - self::assertEquals(5, $this->repo->countList( - new ShortUrlsCountFiltering(null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ANY), - )); - self::assertEquals(1, $this->repo->countList( - new ShortUrlsCountFiltering(null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ALL), - )); + self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ANY))); + self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ALL))); self::assertCount(4, $this->repo->findList( new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['bar', 'baz']), @@ -254,7 +250,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase Ordering::emptyInstance(), null, ['bar', 'baz'], - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY, ))); self::assertCount(2, $this->repo->findList(new ShortUrlsListFiltering( null, @@ -262,14 +258,14 @@ class ShortUrlRepositoryTest extends DatabaseTestCase Ordering::emptyInstance(), null, ['bar', 'baz'], - ShortUrlsParams::TAGS_MODE_ALL, + TagsMode::ALL, ))); self::assertEquals(4, $this->repo->countList(new ShortUrlsCountFiltering(null, ['bar', 'baz']))); self::assertEquals(4, $this->repo->countList( - new ShortUrlsCountFiltering(null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY), + new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ANY), )); self::assertEquals(2, $this->repo->countList( - new ShortUrlsCountFiltering(null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL), + new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ALL), )); self::assertCount(5, $this->repo->findList( @@ -281,7 +277,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase Ordering::emptyInstance(), null, ['foo', 'bar', 'baz'], - ShortUrlsParams::TAGS_MODE_ANY, + TagsMode::ANY, ))); self::assertCount(0, $this->repo->findList(new ShortUrlsListFiltering( null, @@ -289,14 +285,14 @@ class ShortUrlRepositoryTest extends DatabaseTestCase Ordering::emptyInstance(), null, ['foo', 'bar', 'baz'], - ShortUrlsParams::TAGS_MODE_ALL, + TagsMode::ALL, ))); self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz']))); self::assertEquals(5, $this->repo->countList( - new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY), + new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ANY), )); self::assertEquals(0, $this->repo->countList( - new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL), + new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ALL), )); } diff --git a/module/Core/test-db/Repository/TagRepositoryTest.php b/module/Core/test-db/Repository/TagRepositoryTest.php index fe544376..87cd7280 100644 --- a/module/Core/test-db/Repository/TagRepositoryTest.php +++ b/module/Core/test-db/Repository/TagRepositoryTest.php @@ -64,7 +64,7 @@ class TagRepositoryTest extends DatabaseTestCase $this->getEntityManager()->persist(new Tag($name)); } - $apiKey = $filtering?->apiKey(); + $apiKey = $filtering?->apiKey; if ($apiKey !== null) { $this->getEntityManager()->persist($apiKey); } @@ -101,9 +101,9 @@ class TagRepositoryTest extends DatabaseTestCase self::assertCount(count($expectedList), $result); foreach ($expectedList as $index => [$tag, $shortUrlsCount, $visitsCount]) { - self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount()); - self::assertEquals($visitsCount, $result[$index]->visitsCount()); - self::assertEquals($tag, $result[$index]->tag()); + self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount); + self::assertEquals($visitsCount, $result[$index]->visitsCount); + self::assertEquals($tag, $result[$index]->tag); } } diff --git a/module/Core/test/Action/PixelActionTest.php b/module/Core/test/Action/PixelActionTest.php index 3eb1ad79..fdd291a5 100644 --- a/module/Core/test/Action/PixelActionTest.php +++ b/module/Core/test/Action/PixelActionTest.php @@ -37,9 +37,10 @@ class PixelActionTest extends TestCase public function imageIsReturned(): void { $shortCode = 'abc123'; - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn( - ShortUrl::withLongUrl('http://domain.com/foo/bar'), - )->shouldBeCalledOnce(); + $this->urlResolver->resolveEnabledShortUrl( + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''), + )->willReturn(ShortUrl::withLongUrl('http://domain.com/foo/bar')) + ->shouldBeCalledOnce(); $this->requestTracker->trackIfApplicable(Argument::cetera())->shouldBeCalledOnce(); $request = (new ServerRequest())->withAttribute('shortCode', $shortCode); diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 419febec..fb9e4e6a 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -59,7 +59,7 @@ class QrCodeActionTest extends TestCase public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void { $shortCode = 'abc123'; - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, '')) + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, '')) ->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $delegate = $this->prophesize(RequestHandlerInterface::class); @@ -74,7 +74,7 @@ class QrCodeActionTest extends TestCase public function aCorrectRequestReturnsTheQrCodeResponse(): void { $shortCode = 'abc123'; - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, '')) + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, '')) ->willReturn(ShortUrl::createEmpty()) ->shouldBeCalledOnce(); $delegate = $this->prophesize(RequestHandlerInterface::class); @@ -100,7 +100,7 @@ class QrCodeActionTest extends TestCase ): void { $this->options->setFromArray(['format' => $defaultFormat]); $code = 'abc123'; - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn( + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn( ShortUrl::createEmpty(), ); $delegate = $this->prophesize(RequestHandlerInterface::class); @@ -134,7 +134,7 @@ class QrCodeActionTest extends TestCase ): void { $this->options->setFromArray($defaults); $code = 'abc123'; - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn( + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn( ShortUrl::createEmpty(), ); $delegate = $this->prophesize(RequestHandlerInterface::class); @@ -214,7 +214,7 @@ class QrCodeActionTest extends TestCase ->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize]) ->withAttribute('shortCode', $code); - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn( + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn( ShortUrl::withLongUrl('https://shlink.io'), ); $delegate = $this->prophesize(RequestHandlerInterface::class); diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index b3017fad..cde2b9aa 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -54,7 +54,7 @@ class RedirectActionTest extends TestCase $shortCode = 'abc123'; $shortUrl = ShortUrl::withLongUrl(self::LONG_URL); $shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl( - new ShortUrlIdentifier($shortCode, ''), + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''), )->willReturn($shortUrl); $track = $this->requestTracker->trackIfApplicable(Argument::cetera())->will(function (): void { }); @@ -74,7 +74,7 @@ class RedirectActionTest extends TestCase public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void { $shortCode = 'abc123'; - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, '')) + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, '')) ->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $this->requestTracker->trackIfApplicable(Argument::cetera())->shouldNotBeCalled(); diff --git a/module/Core/test/Config/EnvVarsTest.php b/module/Core/test/Config/EnvVarsTest.php index 51a7a088..6d4b1394 100644 --- a/module/Core/test/Config/EnvVarsTest.php +++ b/module/Core/test/Config/EnvVarsTest.php @@ -6,7 +6,6 @@ namespace ShlinkioTest\Shlink\Core\Config; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Config\EnvVars; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use function putenv; @@ -14,92 +13,14 @@ class EnvVarsTest extends TestCase { protected function setUp(): void { - putenv(EnvVars::BASE_PATH . '=the_base_path'); - putenv(EnvVars::DB_NAME . '=shlink'); + putenv(EnvVars::BASE_PATH->value . '=the_base_path'); + putenv(EnvVars::DB_NAME->value . '=shlink'); } protected function tearDown(): void { - putenv(EnvVars::BASE_PATH . '='); - putenv(EnvVars::DB_NAME . '='); - } - - /** @test */ - public function casesReturnsTheSameListEveryTime(): void - { - $list = EnvVars::cases(); - self::assertSame($list, EnvVars::cases()); - self::assertSame([ - EnvVars::DELETE_SHORT_URL_THRESHOLD, - EnvVars::DB_DRIVER, - EnvVars::DB_NAME, - EnvVars::DB_USER, - EnvVars::DB_PASSWORD, - EnvVars::DB_HOST, - EnvVars::DB_UNIX_SOCKET, - EnvVars::DB_PORT, - EnvVars::GEOLITE_LICENSE_KEY, - EnvVars::REDIS_SERVERS, - EnvVars::REDIS_SENTINEL_SERVICE, - EnvVars::MERCURE_PUBLIC_HUB_URL, - EnvVars::MERCURE_INTERNAL_HUB_URL, - EnvVars::MERCURE_JWT_SECRET, - EnvVars::DEFAULT_QR_CODE_SIZE, - EnvVars::DEFAULT_QR_CODE_MARGIN, - EnvVars::DEFAULT_QR_CODE_FORMAT, - EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION, - EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, - EnvVars::RABBITMQ_ENABLED, - EnvVars::RABBITMQ_HOST, - EnvVars::RABBITMQ_PORT, - EnvVars::RABBITMQ_USER, - EnvVars::RABBITMQ_PASSWORD, - EnvVars::RABBITMQ_VHOST, - EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT, - EnvVars::DEFAULT_REGULAR_404_REDIRECT, - EnvVars::DEFAULT_BASE_URL_REDIRECT, - EnvVars::REDIRECT_STATUS_CODE, - EnvVars::REDIRECT_CACHE_LIFETIME, - EnvVars::BASE_PATH, - EnvVars::PORT, - EnvVars::TASK_WORKER_NUM, - EnvVars::WEB_WORKER_NUM, - EnvVars::ANONYMIZE_REMOTE_ADDR, - EnvVars::TRACK_ORPHAN_VISITS, - EnvVars::DISABLE_TRACK_PARAM, - EnvVars::DISABLE_TRACKING, - EnvVars::DISABLE_IP_TRACKING, - EnvVars::DISABLE_REFERRER_TRACKING, - EnvVars::DISABLE_UA_TRACKING, - EnvVars::DISABLE_TRACKING_FROM, - EnvVars::DEFAULT_SHORT_CODES_LENGTH, - EnvVars::IS_HTTPS_ENABLED, - EnvVars::DEFAULT_DOMAIN, - EnvVars::AUTO_RESOLVE_TITLES, - EnvVars::REDIRECT_APPEND_EXTRA_PATH, - EnvVars::TIMEZONE, - EnvVars::VISITS_WEBHOOKS, - EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS, - ], $list); - } - - /** - * @test - * @dataProvider provideInvalidEnvVars - */ - public function exceptionIsThrownWhenTryingToLoadInvalidEnvVar(string $envVar): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid env var: "' . $envVar . '"'); - - EnvVars::{$envVar}(); - } - - public function provideInvalidEnvVars(): iterable - { - yield 'foo' => ['foo']; - yield 'bar' => ['bar']; - yield 'invalid' => ['invalid']; + putenv(EnvVars::BASE_PATH->value . '='); + putenv(EnvVars::DB_NAME->value . '='); } /** @@ -113,10 +34,10 @@ class EnvVarsTest extends TestCase public function provideExistingEnvVars(): iterable { - yield 'DB_NAME' => [EnvVars::DB_NAME(), true]; - yield 'BASE_PATH' => [EnvVars::BASE_PATH(), true]; - yield 'DB_DRIVER' => [EnvVars::DB_DRIVER(), false]; - yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT(), false]; + yield 'DB_NAME' => [EnvVars::DB_NAME, true]; + yield 'BASE_PATH' => [EnvVars::BASE_PATH, true]; + yield 'DB_DRIVER' => [EnvVars::DB_DRIVER, false]; + yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT, false]; } /** @@ -130,11 +51,11 @@ class EnvVarsTest extends TestCase public function provideEnvVarsValues(): iterable { - yield 'DB_NAME without default' => [EnvVars::DB_NAME(), 'shlink', null]; - yield 'DB_NAME with default' => [EnvVars::DB_NAME(), 'shlink', 'foobar']; - yield 'BASE_PATH without default' => [EnvVars::BASE_PATH(), 'the_base_path', null]; - yield 'BASE_PATH with default' => [EnvVars::BASE_PATH(), 'the_base_path', 'foobar']; - yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER(), null, null]; - yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER(), 'foobar', 'foobar']; + yield 'DB_NAME without default' => [EnvVars::DB_NAME, 'shlink', null]; + yield 'DB_NAME with default' => [EnvVars::DB_NAME, 'shlink', 'foobar']; + yield 'BASE_PATH without default' => [EnvVars::BASE_PATH, 'the_base_path', null]; + yield 'BASE_PATH with default' => [EnvVars::BASE_PATH, 'the_base_path', 'foobar']; + yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER, null, null]; + yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER, 'foobar', 'foobar']; } } diff --git a/module/Core/test/EventDispatcher/NotifyVisitToMercureTest.php b/module/Core/test/EventDispatcher/NotifyVisitToMercureTest.php index 0b863b69..a06eaaa1 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToMercureTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToMercureTest.php @@ -17,6 +17,7 @@ use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated; use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToMercure; use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGeneratorInterface; use Shlinkio\Shlink\Core\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\Update; @@ -160,8 +161,8 @@ class NotifyVisitToMercureTest extends TestCase { $visitor = Visitor::emptyInstance(); - yield Visit::TYPE_REGULAR_404 => [Visit::forRegularNotFound($visitor)]; - yield Visit::TYPE_INVALID_SHORT_URL => [Visit::forInvalidShortUrl($visitor)]; - yield Visit::TYPE_BASE_URL => [Visit::forBasePath($visitor)]; + yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor)]; + yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)]; + yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)]; } } diff --git a/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php b/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php index ea4e606d..e86a63cb 100644 --- a/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php +++ b/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php @@ -24,7 +24,7 @@ class ShortUrlNotFoundExceptionTest extends TestCase $expectedAdditional['domain'] = $domain; } - $e = ShortUrlNotFoundException::fromNotFound(new ShortUrlIdentifier($shortCode, $domain)); + $e = ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain)); self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); diff --git a/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php b/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php index 14378b4f..779ec351 100644 --- a/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php +++ b/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php @@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer; use function Shlinkio\Shlink\Common\json_decode; @@ -95,7 +96,7 @@ class MercureUpdatesGeneratorTest extends TestCase 'date' => $orphanVisit->getDate()->toAtomString(), 'potentialBot' => false, 'visitedUrl' => $orphanVisit->visitedUrl(), - 'type' => $orphanVisit->type(), + 'type' => $orphanVisit->type()->value, ], ], json_decode($update->getData())); } @@ -104,8 +105,8 @@ class MercureUpdatesGeneratorTest extends TestCase { $visitor = Visitor::emptyInstance(); - yield Visit::TYPE_REGULAR_404 => [Visit::forRegularNotFound($visitor)]; - yield Visit::TYPE_INVALID_SHORT_URL => [Visit::forInvalidShortUrl($visitor)]; - yield Visit::TYPE_BASE_URL => [Visit::forBasePath($visitor)]; + yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor)]; + yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)]; + yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)]; } } diff --git a/module/Core/test/Model/VisitorTest.php b/module/Core/test/Model/VisitorTest.php index 50c277c4..92a46a16 100644 --- a/module/Core/test/Model/VisitorTest.php +++ b/module/Core/test/Model/VisitorTest.php @@ -24,9 +24,9 @@ class VisitorTest extends TestCase $visitor = new Visitor(...$params); ['userAgent' => $userAgent, 'referer' => $referer, 'remoteAddress' => $remoteAddress] = $expected; - self::assertEquals($userAgent, $visitor->getUserAgent()); - self::assertEquals($referer, $visitor->getReferer()); - self::assertEquals($remoteAddress, $visitor->getRemoteAddress()); + self::assertEquals($userAgent, $visitor->userAgent); + self::assertEquals($referer, $visitor->referer); + self::assertEquals($remoteAddress, $visitor->remoteAddress); } public function provideParams(): iterable @@ -89,11 +89,11 @@ class VisitorTest extends TestCase ])); self::assertNotSame($visitor, $normalizedVisitor); - self::assertEmpty($normalizedVisitor->getUserAgent()); - self::assertNotEmpty($visitor->getUserAgent()); - self::assertEmpty($normalizedVisitor->getReferer()); - self::assertNotEmpty($visitor->getReferer()); - self::assertNull($normalizedVisitor->getRemoteAddress()); - self::assertNotNull($visitor->getRemoteAddress()); + self::assertEmpty($normalizedVisitor->userAgent); + self::assertNotEmpty($visitor->userAgent); + self::assertEmpty($normalizedVisitor->referer); + self::assertNotEmpty($visitor->referer); + self::assertNull($normalizedVisitor->remoteAddress); + self::assertNotNull($visitor->remoteAddress); } } diff --git a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php index 6c03d7b5..cd4d6193 100644 --- a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php @@ -55,7 +55,7 @@ class DeleteShortUrlServiceTest extends TestCase $this->shortCode, )); - $service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode)); + $service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode)); } /** @test */ @@ -66,7 +66,7 @@ class DeleteShortUrlServiceTest extends TestCase $remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null); $flush = $this->em->flush()->willReturn(null); - $service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode), true); + $service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode), true); $remove->shouldHaveBeenCalledOnce(); $flush->shouldHaveBeenCalledOnce(); @@ -80,7 +80,7 @@ class DeleteShortUrlServiceTest extends TestCase $remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null); $flush = $this->em->flush()->willReturn(null); - $service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode)); + $service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode)); $remove->shouldHaveBeenCalledOnce(); $flush->shouldHaveBeenCalledOnce(); @@ -94,7 +94,7 @@ class DeleteShortUrlServiceTest extends TestCase $remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null); $flush = $this->em->flush()->willReturn(null); - $service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode)); + $service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode)); $remove->shouldHaveBeenCalledOnce(); $flush->shouldHaveBeenCalledOnce(); diff --git a/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php index 70857e5e..bdccfa3f 100644 --- a/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php +++ b/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php @@ -91,7 +91,7 @@ class ShortUrlResolverTest extends TestCase )->willReturn($shortUrl); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $result = $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode)); + $result = $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode)); self::assertSame($shortUrl, $result); $findOneByShortCode->shouldHaveBeenCalledOnce(); @@ -116,7 +116,7 @@ class ShortUrlResolverTest extends TestCase $findOneByShortCode->shouldBeCalledOnce(); $getRepo->shouldBeCalledOnce(); - $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode)); + $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode)); } public function provideDisabledShortUrls(): iterable diff --git a/module/Core/test/Service/ShortUrlServiceTest.php b/module/Core/test/Service/ShortUrlServiceTest.php index b07d4df9..90000423 100644 --- a/module/Core/test/Service/ShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrlServiceTest.php @@ -88,7 +88,7 @@ class ShortUrlServiceTest extends TestCase $shortUrl = ShortUrl::withLongUrl($originalLongUrl); $findShortUrl = $this->urlResolver->resolveShortUrl( - new ShortUrlIdentifier('abc123'), + ShortUrlIdentifier::fromShortCodeAndDomain('abc123'), $apiKey, )->willReturn($shortUrl); $flush = $this->em->flush()->willReturn(null); @@ -97,7 +97,11 @@ class ShortUrlServiceTest extends TestCase $shortUrlEdit, ); - $result = $this->service->updateShortUrl(new ShortUrlIdentifier('abc123'), $shortUrlEdit, $apiKey); + $result = $this->service->updateShortUrl( + ShortUrlIdentifier::fromShortCodeAndDomain('abc123'), + $shortUrlEdit, + $apiKey, + ); self::assertSame($shortUrl, $result); self::assertEquals($shortUrlEdit->validSince(), $shortUrl->getValidSince()); diff --git a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php index 336526b1..2675b04a 100644 --- a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php +++ b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php @@ -10,6 +10,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; +use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; use Shlinkio\Shlink\Core\ShortUrl\Paginator\Adapter\ShortUrlRepositoryAdapter; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering; @@ -49,7 +50,7 @@ class ShortUrlRepositoryAdapterTest extends TestCase $dateRange = $params->dateRange(); $this->repo->findList( - new ShortUrlsListFiltering(10, 5, $orderBy, $searchTerm, $tags, ShortUrlsParams::TAGS_MODE_ANY, $dateRange), + new ShortUrlsListFiltering(10, 5, $orderBy, $searchTerm, $tags, TagsMode::ANY, $dateRange), )->shouldBeCalledOnce(); $adapter->getSlice(5, 10); } @@ -75,7 +76,7 @@ class ShortUrlRepositoryAdapterTest extends TestCase $dateRange = $params->dateRange(); $this->repo->countList( - new ShortUrlsCountFiltering($searchTerm, $tags, ShortUrlsParams::TAGS_MODE_ANY, $dateRange, $apiKey), + new ShortUrlsCountFiltering($searchTerm, $tags, TagsMode::ANY, $dateRange, $apiKey), )->shouldBeCalledOnce(); $adapter->getNbResults(); } diff --git a/module/Core/test/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapterTest.php b/module/Core/test/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapterTest.php index 4c4c00e5..ba1d2767 100644 --- a/module/Core/test/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapterTest.php +++ b/module/Core/test/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapterTest.php @@ -39,7 +39,7 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase { $expectedCount = 5; $repoCount = $this->repo->countNonOrphanVisits( - new VisitsCountFiltering($this->params->getDateRange(), $this->params->excludeBots(), $this->apiKey), + new VisitsCountFiltering($this->params->dateRange, $this->params->excludeBots, $this->apiKey), )->willReturn($expectedCount); $result = $this->adapter->getNbResults(); @@ -57,8 +57,8 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase $visitor = Visitor::emptyInstance(); $list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)]; $repoFind = $this->repo->findNonOrphanVisits(new VisitsListFiltering( - $this->params->getDateRange(), - $this->params->excludeBots(), + $this->params->dateRange, + $this->params->excludeBots, $this->apiKey, $limit, $offset, diff --git a/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php b/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php index 0ea91f29..6709c538 100644 --- a/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php +++ b/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php @@ -35,7 +35,7 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase { $expectedCount = 5; $repoCount = $this->repo->countOrphanVisits( - new VisitsCountFiltering($this->params->getDateRange()), + new VisitsCountFiltering($this->params->dateRange), )->willReturn($expectedCount); $result = $this->adapter->getNbResults(); @@ -53,7 +53,7 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase $visitor = Visitor::emptyInstance(); $list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)]; $repoFind = $this->repo->findOrphanVisits( - new VisitsListFiltering($this->params->getDateRange(), $this->params->excludeBots(), null, $limit, $offset), + new VisitsListFiltering($this->params->dateRange, $this->params->excludeBots, null, $limit, $offset), )->willReturn($list); $result = $this->adapter->getSlice($offset, $limit); diff --git a/module/Core/test/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapterTest.php b/module/Core/test/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapterTest.php index 04e17bc6..ae9a2a64 100644 --- a/module/Core/test/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapterTest.php +++ b/module/Core/test/Visit/Paginator/Adapter/ShortUrlVisitsPaginatorAdapterTest.php @@ -68,7 +68,7 @@ class ShortUrlVisitsPaginatorAdapterTest extends TestCase { return new ShortUrlVisitsPaginatorAdapter( $this->repo->reveal(), - new ShortUrlIdentifier(''), + ShortUrlIdentifier::fromShortCodeAndDomain(''), VisitsParams::fromRawData([]), $apiKey, ); diff --git a/module/Core/test/Visit/Transformer/OrphanVisitDataTransformerTest.php b/module/Core/test/Visit/Transformer/OrphanVisitDataTransformerTest.php index c836cd7c..2d2561bd 100644 --- a/module/Core/test/Visit/Transformer/OrphanVisitDataTransformerTest.php +++ b/module/Core/test/Visit/Transformer/OrphanVisitDataTransformerTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer; use Shlinkio\Shlink\IpGeolocation\Model\Location; @@ -44,7 +45,7 @@ class OrphanVisitDataTransformerTest extends TestCase 'visitLocation' => null, 'potentialBot' => false, 'visitedUrl' => '', - 'type' => Visit::TYPE_BASE_URL, + 'type' => VisitType::BASE_URL->value, ], ]; yield 'invalid short url visit' => [ @@ -60,7 +61,7 @@ class OrphanVisitDataTransformerTest extends TestCase 'visitLocation' => null, 'potentialBot' => false, 'visitedUrl' => 'https://example.com/foo', - 'type' => Visit::TYPE_INVALID_SHORT_URL, + 'type' => VisitType::INVALID_SHORT_URL->value, ], ]; yield 'regular 404 visit' => [ @@ -78,7 +79,7 @@ class OrphanVisitDataTransformerTest extends TestCase 'visitLocation' => $location, 'potentialBot' => false, 'visitedUrl' => 'https://doma.in/foo/bar', - 'type' => Visit::TYPE_REGULAR_404, + 'type' => VisitType::REGULAR_404->value, ], ]; } diff --git a/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKeyRole.php b/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKeyRole.php index b7787b1a..8df324a4 100644 --- a/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKeyRole.php +++ b/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKeyRole.php @@ -6,7 +6,9 @@ namespace Shlinkio\Shlink\Rest; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder; +use Doctrine\ORM\Mapping\Builder\FieldBuilder; use Doctrine\ORM\Mapping\ClassMetadata; +use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\Entity\ApiKey; use function Shlinkio\Shlink\Core\determineTableName; @@ -22,11 +24,14 @@ return static function (ClassMetadata $metadata, array $emConfig): void { ->option('unsigned', true) ->build(); - $builder->createField('roleName', Types::STRING) - ->columnName('role_name') - ->length(255) - ->nullable(false) - ->build(); + (new FieldBuilder($builder, [ + 'fieldName' => 'roleName', + 'type' => Types::STRING, + 'enumType' => Role::class, + ]))->columnName('role_name') + ->length(255) + ->nullable(false) + ->build(); $builder->createField('meta', Types::JSON) ->columnName('meta') diff --git a/module/Rest/src/Action/AbstractRestAction.php b/module/Rest/src/Action/AbstractRestAction.php index da8b6d80..f330bab1 100644 --- a/module/Rest/src/Action/AbstractRestAction.php +++ b/module/Rest/src/Action/AbstractRestAction.php @@ -8,8 +8,6 @@ use Fig\Http\Message\RequestMethodInterface; use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Server\RequestHandlerInterface; -use function array_merge; - abstract class AbstractRestAction implements RequestHandlerInterface, RequestMethodInterface, StatusCodeInterface { protected const ROUTE_PATH = ''; @@ -19,7 +17,7 @@ abstract class AbstractRestAction implements RequestHandlerInterface, RequestMet { return [ 'name' => static::class, - 'middleware' => array_merge($prevMiddleware, [static::class], $postMiddleware), + 'middleware' => [...$prevMiddleware, static::class, ...$postMiddleware], 'path' => static::ROUTE_PATH, 'allowed_methods' => static::ROUTE_ALLOWED_METHODS, ]; diff --git a/module/Rest/src/Action/Tag/ListTagsAction.php b/module/Rest/src/Action/Tag/ListTagsAction.php index ab81400c..d52436d2 100644 --- a/module/Rest/src/Action/Tag/ListTagsAction.php +++ b/module/Rest/src/Action/Tag/ListTagsAction.php @@ -32,7 +32,7 @@ class ListTagsAction extends AbstractRestAction $params = TagsParams::fromRawData($request->getQueryParams()); $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); - if (! $params->withStats()) { + if (! $params->withStats) { return new JsonResponse([ 'tags' => $this->serializePaginator($this->tagService->listTags($params, $apiKey)), ]); @@ -41,7 +41,7 @@ class ListTagsAction extends AbstractRestAction // This part is deprecated. To get tags with stats, the /tags/stats endpoint should be used instead $tagsInfo = $this->tagService->tagsInfo($params, $apiKey); $rawTags = $this->serializePaginator($tagsInfo, null, 'stats'); - $rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag()); + $rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag); return new JsonResponse(['tags' => $rawTags]); } diff --git a/module/Rest/src/ApiKey/Model/ApiKeyMeta.php b/module/Rest/src/ApiKey/Model/ApiKeyMeta.php index 39b5dca1..430221a2 100644 --- a/module/Rest/src/ApiKey/Model/ApiKeyMeta.php +++ b/module/Rest/src/ApiKey/Model/ApiKeyMeta.php @@ -8,11 +8,13 @@ use Cake\Chronos\Chronos; final class ApiKeyMeta { + /** + * @param RoleDefinition[] $roleDefinitions + */ private function __construct( - private ?string $name, - private ?Chronos $expirationDate, - /** @var RoleDefinition[] */ - private array $roleDefinitions, + public readonly ?string $name, + public readonly ?Chronos $expirationDate, + public readonly array $roleDefinitions, ) { } @@ -35,22 +37,4 @@ final class ApiKeyMeta { return new self(null, null, $roleDefinitions); } - - public function name(): ?string - { - return $this->name; - } - - public function expirationDate(): ?Chronos - { - return $this->expirationDate; - } - - /** - * @return RoleDefinition[] - */ - public function roleDefinitions(): array - { - return $this->roleDefinitions; - } } diff --git a/module/Rest/src/ApiKey/Model/RoleDefinition.php b/module/Rest/src/ApiKey/Model/RoleDefinition.php index fdd4d5cb..63c9b72a 100644 --- a/module/Rest/src/ApiKey/Model/RoleDefinition.php +++ b/module/Rest/src/ApiKey/Model/RoleDefinition.php @@ -9,7 +9,7 @@ use Shlinkio\Shlink\Rest\ApiKey\Role; final class RoleDefinition { - private function __construct(private string $roleName, private array $meta) + private function __construct(public readonly Role $role, public readonly array $meta) { } @@ -25,14 +25,4 @@ final class RoleDefinition ['domain_id' => $domain->getId(), 'authority' => $domain->getAuthority()], ); } - - public function roleName(): string - { - return $this->roleName; - } - - public function meta(): array - { - return $this->meta; - } } diff --git a/module/Rest/src/ApiKey/Role.php b/module/Rest/src/ApiKey/Role.php index 557abd00..64803969 100644 --- a/module/Rest/src/ApiKey/Role.php +++ b/module/Rest/src/ApiKey/Role.php @@ -2,6 +2,8 @@ declare(strict_types=1); +// phpcs:disable +// TODO Enable coding style checks again once code sniffer 3.7 is released https://github.com/squizlabs/PHP_CodeSniffer/issues/3474 namespace Shlinkio\Shlink\Rest\ApiKey; use Happyr\DoctrineSpecification\Spec; @@ -12,30 +14,24 @@ use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToDomain; use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToDomainInlined; use Shlinkio\Shlink\Rest\Entity\ApiKeyRole; -class Role +enum Role: string { - public const AUTHORED_SHORT_URLS = 'AUTHORED_SHORT_URLS'; - public const DOMAIN_SPECIFIC = 'DOMAIN_SPECIFIC'; - private const ROLE_FRIENDLY_NAMES = [ - self::AUTHORED_SHORT_URLS => 'Author only', - self::DOMAIN_SPECIFIC => 'Domain only', - ]; + case AUTHORED_SHORT_URLS = 'AUTHORED_SHORT_URLS'; + case DOMAIN_SPECIFIC = 'DOMAIN_SPECIFIC'; public static function toSpec(ApiKeyRole $role, ?string $context = null): Specification { - return match ($role->name()) { + return match ($role->role()) { self::AUTHORED_SHORT_URLS => new BelongsToApiKey($role->apiKey(), $context), self::DOMAIN_SPECIFIC => new BelongsToDomain(self::domainIdFromMeta($role->meta()), $context), - default => Spec::andX(), }; } public static function toInlinedSpec(ApiKeyRole $role): Specification { - return match ($role->name()) { + return match ($role->role()) { self::AUTHORED_SHORT_URLS => Spec::andX(new BelongsToApiKeyInlined($role->apiKey())), self::DOMAIN_SPECIFIC => Spec::andX(new BelongsToDomainInlined(self::domainIdFromMeta($role->meta()))), - default => Spec::andX(), }; } @@ -49,8 +45,11 @@ class Role return $meta['authority'] ?? ''; } - public static function toFriendlyName(string $roleName): string + public static function toFriendlyName(Role $role): string { - return self::ROLE_FRIENDLY_NAMES[$roleName] ?? ''; + return match ($role) { + self::AUTHORED_SHORT_URLS => 'Author only', + self::DOMAIN_SPECIFIC => 'Domain only', + }; } } diff --git a/module/Rest/src/Entity/ApiKey.php b/module/Rest/src/Entity/ApiKey.php index 2940bc69..261baee4 100644 --- a/module/Rest/src/Entity/ApiKey.php +++ b/module/Rest/src/Entity/ApiKey.php @@ -44,8 +44,8 @@ class ApiKey extends AbstractEntity public static function fromMeta(ApiKeyMeta $meta): self { - $apiKey = new self($meta->name(), $meta->expirationDate()); - foreach ($meta->roleDefinitions() as $roleDefinition) { + $apiKey = new self($meta->name, $meta->expirationDate); + foreach ($meta->roleDefinitions as $roleDefinition) { $apiKey->registerRole($roleDefinition); } @@ -113,45 +113,40 @@ class ApiKey extends AbstractEntity return $this->roles->isEmpty(); } - public function hasRole(string $roleName): bool + public function hasRole(Role $role): bool { - return $this->roles->containsKey($roleName); + return $this->roles->containsKey($role->value); } - public function getRoleMeta(string $roleName): array + public function getRoleMeta(Role $role): array { - /** @var ApiKeyRole|null $role */ - $role = $this->roles->get($roleName); - return $role?->meta() ?? []; + /** @var ApiKeyRole|null $apiKeyRole */ + $apiKeyRole = $this->roles->get($role->value); + return $apiKeyRole?->meta() ?? []; } /** * @template T - * @param callable(string $roleName, array $meta): T $fun + * @param callable(Role $role, array $meta): T $fun * @return T[] */ public function mapRoles(callable $fun): array { - return $this->roles->map(fn (ApiKeyRole $role) => $fun($role->name(), $role->meta()))->getValues(); + return $this->roles->map(fn (ApiKeyRole $role) => $fun($role->role(), $role->meta()))->getValues(); } public function registerRole(RoleDefinition $roleDefinition): void { - $roleName = $roleDefinition->roleName(); - $meta = $roleDefinition->meta(); + $role = $roleDefinition->role; + $meta = $roleDefinition->meta; - if ($this->hasRole($roleName)) { - /** @var ApiKeyRole $role */ - $role = $this->roles->get($roleName); - $role->updateMeta($meta); + if ($this->hasRole($role)) { + /** @var ApiKeyRole $apiKeyRole */ + $apiKeyRole = $this->roles->get($role); + $apiKeyRole->updateMeta($meta); } else { - $role = new ApiKeyRole($roleDefinition->roleName(), $roleDefinition->meta(), $this); - $this->roles[$roleName] = $role; + $apiKeyRole = new ApiKeyRole($roleDefinition->role, $roleDefinition->meta, $this); + $this->roles[$role->value] = $apiKeyRole; } } - - public function removeRole(string $roleName): void - { - $this->roles->remove($roleName); - } } diff --git a/module/Rest/src/Entity/ApiKeyRole.php b/module/Rest/src/Entity/ApiKeyRole.php index 1155c37b..8491cfce 100644 --- a/module/Rest/src/Entity/ApiKeyRole.php +++ b/module/Rest/src/Entity/ApiKeyRole.php @@ -5,14 +5,15 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Entity; use Shlinkio\Shlink\Common\Entity\AbstractEntity; +use Shlinkio\Shlink\Rest\ApiKey\Role; class ApiKeyRole extends AbstractEntity { - public function __construct(private string $roleName, private array $meta, private ApiKey $apiKey) + public function __construct(private Role $roleName, private array $meta, private ApiKey $apiKey) { } - public function name(): string + public function role(): Role { return $this->roleName; } diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php index 25f1fbe5..7b911817 100644 --- a/module/Rest/src/Middleware/AuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php @@ -49,7 +49,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa throw VerifyAuthenticationException::forInvalidApiKey(); } - return $handler->handle($request->withAttribute(ApiKey::class, $result->apiKey())); + return $handler->handle($request->withAttribute(ApiKey::class, $result->apiKey)); } public static function apiKeyFromRequest(Request $request): ApiKey diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index b0d63dc7..d6a51a0c 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -11,7 +11,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use function array_merge; use function implode; class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterface @@ -45,7 +44,7 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa ]; // Options requests should always be empty and have a 204 status code - return EmptyResponse::withHeaders(array_merge($response->getHeaders(), $corsHeaders)); + return EmptyResponse::withHeaders([...$response->getHeaders(), ...$corsHeaders]); } private function resolveCorsAllowedMethods(ResponseInterface $response): string diff --git a/module/Rest/src/Service/ApiKeyCheckResult.php b/module/Rest/src/Service/ApiKeyCheckResult.php index 2caee4e1..ff74fb79 100644 --- a/module/Rest/src/Service/ApiKeyCheckResult.php +++ b/module/Rest/src/Service/ApiKeyCheckResult.php @@ -8,7 +8,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; final class ApiKeyCheckResult { - public function __construct(private ?ApiKey $apiKey = null) + public function __construct(public readonly ?ApiKey $apiKey = null) { } @@ -16,9 +16,4 @@ final class ApiKeyCheckResult { return $this->apiKey !== null && $this->apiKey->isValid(); } - - public function apiKey(): ?ApiKey - { - return $this->apiKey; - } } diff --git a/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php b/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php index 55828368..05212fe7 100644 --- a/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php +++ b/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php @@ -44,9 +44,9 @@ class DomainRedirectsRequestTest extends TestCase $notFound = $request->toNotFoundRedirects($defaults); self::assertEquals($expectedAuthority, $request->authority()); - self::assertEquals($expectedBaseUrlRedirect, $notFound->baseUrlRedirect()); - self::assertEquals($expectedRegular404Redirect, $notFound->regular404Redirect()); - self::assertEquals($expectedInvalidShortUrlRedirect, $notFound->invalidShortUrlRedirect()); + self::assertEquals($expectedBaseUrlRedirect, $notFound->baseUrlRedirect); + self::assertEquals($expectedRegular404Redirect, $notFound->regular404Redirect); + self::assertEquals($expectedInvalidShortUrlRedirect, $notFound->invalidShortUrlRedirect); } public function provideValidData(): iterable diff --git a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php index 04ffb107..19422d9d 100644 --- a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php @@ -36,9 +36,11 @@ class ResolveShortUrlActionTest extends TestCase { $shortCode = 'abc123'; $apiKey = ApiKey::create(); - $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode), $apiKey)->willReturn( - ShortUrl::withLongUrl('http://domain.com/foo/bar'), - )->shouldBeCalledOnce(); + $this->urlResolver->resolveShortUrl( + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + $apiKey, + )->willReturn(ShortUrl::withLongUrl('http://domain.com/foo/bar')) + ->shouldBeCalledOnce(); $request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withAttribute(ApiKey::class, $apiKey); $response = $this->action->handle($request); diff --git a/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php b/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php index 6e982aec..50cabe8e 100644 --- a/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php @@ -38,7 +38,7 @@ class ShortUrlVisitsActionTest extends TestCase { $shortCode = 'abc123'; $this->visitsHelper->visitsForShortUrl( - new ShortUrlIdentifier($shortCode), + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), Argument::type(VisitsParams::class), Argument::type(ApiKey::class), )->willReturn(new Paginator(new ArrayAdapter([]))) @@ -52,7 +52,7 @@ class ShortUrlVisitsActionTest extends TestCase public function paramsAreReadFromQuery(): void { $shortCode = 'abc123'; - $this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), new VisitsParams( + $this->visitsHelper->visitsForShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), new VisitsParams( DateRange::withEndDate(Chronos::parse('2016-01-01 00:00:00')), 3, 10, diff --git a/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php b/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php index 8e6a58ad..ba27a02f 100644 --- a/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php +++ b/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php @@ -16,8 +16,8 @@ class RoleDefinitionTest extends TestCase { $definition = RoleDefinition::forAuthoredShortUrls(); - self::assertEquals(Role::AUTHORED_SHORT_URLS, $definition->roleName()); - self::assertEquals([], $definition->meta()); + self::assertEquals(Role::AUTHORED_SHORT_URLS, $definition->role); + self::assertEquals([], $definition->meta); } /** @test */ @@ -26,7 +26,7 @@ class RoleDefinitionTest extends TestCase $domain = Domain::withAuthority('foo.com')->setId('123'); $definition = RoleDefinition::forDomain($domain); - self::assertEquals(Role::DOMAIN_SPECIFIC, $definition->roleName()); - self::assertEquals(['domain_id' => '123', 'authority' => 'foo.com'], $definition->meta()); + self::assertEquals(Role::DOMAIN_SPECIFIC, $definition->role); + self::assertEquals(['domain_id' => '123', 'authority' => 'foo.com'], $definition->meta); } } diff --git a/module/Rest/test/ApiKey/RoleTest.php b/module/Rest/test/ApiKey/RoleTest.php index 7ee23076..f3cc64b2 100644 --- a/module/Rest/test/ApiKey/RoleTest.php +++ b/module/Rest/test/ApiKey/RoleTest.php @@ -30,7 +30,6 @@ class RoleTest extends TestCase { $apiKey = ApiKey::create(); - yield 'invalid role' => [new ApiKeyRole('invalid', [], $apiKey), Spec::andX()]; yield 'author role' => [ new ApiKeyRole(Role::AUTHORED_SHORT_URLS, [], $apiKey), new BelongsToApiKey($apiKey), @@ -54,7 +53,6 @@ class RoleTest extends TestCase { $apiKey = ApiKey::create(); - yield 'invalid role' => [new ApiKeyRole('invalid', [], $apiKey), Spec::andX()]; yield 'author role' => [ new ApiKeyRole(Role::AUTHORED_SHORT_URLS, [], $apiKey), Spec::andX(new BelongsToApiKeyInlined($apiKey)), @@ -101,15 +99,14 @@ class RoleTest extends TestCase * @test * @dataProvider provideRoleNames */ - public function getsExpectedRoleFriendlyName(string $roleName, string $expectedFriendlyName): void + public function getsExpectedRoleFriendlyName(Role $roleName, string $expectedFriendlyName): void { self::assertEquals($expectedFriendlyName, Role::toFriendlyName($roleName)); } public function provideRoleNames(): iterable { - yield 'unknown' => ['unknown', '']; - yield Role::AUTHORED_SHORT_URLS => [Role::AUTHORED_SHORT_URLS, 'Author only']; - yield Role::DOMAIN_SPECIFIC => [Role::DOMAIN_SPECIFIC, 'Domain only']; + yield Role::AUTHORED_SHORT_URLS->value => [Role::AUTHORED_SHORT_URLS, 'Author only']; + yield Role::DOMAIN_SPECIFIC->value => [Role::DOMAIN_SPECIFIC, 'Domain only']; } } diff --git a/module/Rest/test/Service/ApiKeyServiceTest.php b/module/Rest/test/Service/ApiKeyServiceTest.php index de17d8bd..aba79036 100644 --- a/module/Rest/test/Service/ApiKeyServiceTest.php +++ b/module/Rest/test/Service/ApiKeyServiceTest.php @@ -46,7 +46,7 @@ class ApiKeyServiceTest extends TestCase self::assertEquals($date, $key->getExpirationDate()); self::assertEquals($name, $key->name()); foreach ($roles as $roleDefinition) { - self::assertTrue($key->hasRole($roleDefinition->roleName())); + self::assertTrue($key->hasRole($roleDefinition->role)); } } @@ -77,7 +77,7 @@ class ApiKeyServiceTest extends TestCase $result = $this->service->check('12345'); self::assertFalse($result->isValid()); - self::assertSame($invalidKey, $result->apiKey()); + self::assertSame($invalidKey, $result->apiKey); } public function provideInvalidApiKeys(): iterable @@ -100,7 +100,7 @@ class ApiKeyServiceTest extends TestCase $result = $this->service->check('12345'); self::assertTrue($result->isValid()); - self::assertSame($apiKey, $result->apiKey()); + self::assertSame($apiKey, $result->apiKey); } /** @test */