Merge pull request #2608 from acelaya-forks/mago-config

Extend mago config from shlinkio/shlink-mago-config
This commit is contained in:
Alejandro Celaya
2026-05-22 18:22:27 +02:00
committed by GitHub
56 changed files with 355 additions and 303 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ build.sh export-ignore
CHANGELOG.md export-ignore
docker-compose.yml export-ignore
indocker export-ignore
phpcs.xml export-ignore
mago.toml export-ignore
phpunit.xml.dist export-ignore
phpunit-api.xml export-ignore
phpunit-db.xml export-ignore
+1
View File
@@ -69,6 +69,7 @@
"phpunit/php-code-coverage": "^13.0",
"phpunit/phpcov": "^12.0",
"phpunit/phpunit": "^13.0",
"shlinkio/shlink-mago-config": "^1.0",
"shlinkio/shlink-test-utils": "^4.5",
"symfony/var-dumper": "^8.0",
"veewee/composer-run-parallel": "^1.4"
+1 -74
View File
@@ -3,6 +3,7 @@
# For full documentation, see https://mago.carthage.software/tools/overview
version = "1"
php-version = "8.4.0"
extends = "vendor/shlinkio/shlink-mago-config/mago.toml"
[source]
workspace = "."
@@ -12,27 +13,6 @@ excludes = ["vendor"]
[source.glob]
literal-separator = true
[formatter]
print-width = 120
tab-width = 4
use-tabs = false
null-type-hint = "null_pipe_last"
space-before-arrow-function-parameter-list-parenthesis = true
break-promoted-properties-list = false
parameter-attribute-on-new-line = false
preserve-breaking-member-access-chain = true
preserve-breaking-member-access-chain-first-method-on-same-line = true
preserve-breaking-argument-list = true
preserve-breaking-array-like = true
preserve-breaking-parameter-list = true
preserve-breaking-attribute-list = true
preserve-breaking-conditional-expression = true
preserve-breaking-condition-expression = true
preserve-redundant-logical-binary-expression-parentheses = true
[linter]
integrations = ["phpunit"]
[linter.rules]
no-literal-password = {
exclude = [
@@ -71,62 +51,9 @@ no-empty-loop = {
"module/Rest/test-db"
]
}
prefer-static-closure = { level = "warning" }
array-style = { level = "warning" }
prefer-early-continue = { max_allowed_statements = 2 }
no-fully-qualified-global-class-like = { enabled = true, level = "warning" }
no-fully-qualified-global-constant = { enabled = true, level = "warning" }
no-fully-qualified-global-function = { enabled = true, level = "warning" }
ambiguous-constant-access = { enabled = true, level = "warning" }
ambiguous-function-call = {
enabled = true,
level = "warning",
exclude = [
"module/Core/functions",
"module/Core/config/entities-mappings"
]
}
no-multi-assignments = { enabled = false }
no-assign-in-condition = { enabled = false }
literal-named-argument = { enabled = false }
tagged-fixme = { enabled = false }
tagged-todo = { enabled = false }
no-boolean-flag-parameter = { enabled = false }
no-else-clause = { enabled = false }
no-isset = { enabled = false }
no-empty = { enabled = false }
no-shorthand-ternary = { enabled = false }
no-ini-set = { enabled = false }
too-many-enum-cases = { enabled = false }
function-name = { enabled = false }
# TODO Fine tune these
too-many-methods = { enabled = false }
too-many-properties = { enabled = false }
excessive-parameter-list = { enabled = false }
cyclomatic-complexity = { enabled = false }
kan-defect = { enabled = false }
halstead = { effort-threshold = 7000, enabled = false }
# TODO Enable this
sensitive-parameter = { enabled = false }
[analyzer]
plugins = []
find-unused-definitions = true
find-unused-expressions = true
analyze-dead-code = false
memoize-properties = true
check-throws = true
unchecked-exceptions = ["Error", "LogicException"]
unchecked-exception-classes = []
check-missing-override = true
find-unused-parameters = true
strict-list-index-checks = true
strict-array-index-existence = false
allow-array-truthy-operand = false
no-boolean-literal-comparison = false
check-missing-type-hints = false
register-super-globals = true
+3 -3
View File
@@ -39,12 +39,12 @@ final class ShlinkTable
string|null $headerTitle = null,
): void {
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
$tableRows = $this->withRowSeparators ? $this->addRowSeparators($rows) : $rows;
$table = clone $this->baseTable;
$table->setStyle($style)
$table
->setStyle($style)
->setHeaders($headers)
->setRows($tableRows)
->setFooterTitle($footerTitle)
+2 -1
View File
@@ -23,7 +23,8 @@ class ShlinkTableTest extends TestCase
$footerTitle = 'Footer';
$baseTable = $this->createMock(Table::class);
$baseTable->expects($this->once())
$baseTable
->expects($this->once())
->method('setStyle')
->with(
$this->isInstanceOf(TableStyle::class),
@@ -11,36 +11,38 @@ use Doctrine\ORM\Mapping\ClassMetadata;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('domains')
->setCustomRepositoryClass(Domain\Repository\DomainRepository::class);
$builder->setTable('domains')->setCustomRepositoryClass(Domain\Repository\DomainRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
fieldWithUtf8Charset($builder->createField('authority', Types::STRING), $emConfig)->unique()
->build();
fieldWithUtf8Charset($builder->createField('authority', Types::STRING), $emConfig)->unique()->build();
fieldWithUtf8Charset($builder->createField('baseUrlRedirect', Types::TEXT), $emConfig)->columnName(
'base_url_redirect',
)
fieldWithUtf8Charset($builder->createField('baseUrlRedirect', Types::TEXT), $emConfig)
->columnName(
'base_url_redirect',
)
->nullable()
->length(2048)
->build();
fieldWithUtf8Charset($builder->createField('regular404Redirect', Types::TEXT), $emConfig)->columnName(
'regular_not_found_redirect',
)
fieldWithUtf8Charset($builder->createField('regular404Redirect', Types::TEXT), $emConfig)
->columnName(
'regular_not_found_redirect',
)
->nullable()
->length(2048)
->build();
fieldWithUtf8Charset($builder->createField('invalidShortUrlRedirect', Types::TEXT), $emConfig)->columnName(
'invalid_short_url_redirect',
)
fieldWithUtf8Charset($builder->createField('invalidShortUrlRedirect', Types::TEXT), $emConfig)
->columnName(
'invalid_short_url_redirect',
)
->nullable()
->length(2048)
->build();
@@ -16,18 +16,21 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->setTable('geolocation_db_updates');
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
$builder->createField('dateCreated', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('dateCreated', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('date_created')
->build();
$builder->createField('dateUpdated', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('dateUpdated', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('date_updated')
->nullable()
->build();
@@ -36,20 +39,24 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
'fieldName' => 'status',
'type' => Types::STRING,
'enumType' => GeolocationDbUpdateStatus::class,
])->columnName('status')
])
->columnName('status')
->length(128)
->build();
fieldWithUtf8Charset($builder->createField('error', Types::STRING), $emConfig)->columnName('error')
fieldWithUtf8Charset($builder->createField('error', Types::STRING), $emConfig)
->columnName('error')
->length(1024)
->nullable()
->build();
fieldWithUtf8Charset($builder->createField('reason', Types::STRING), $emConfig)->columnName('reason')
fieldWithUtf8Charset($builder->createField('reason', Types::STRING), $emConfig)
->columnName('reason')
->length(1024)
->build();
fieldWithUtf8Charset($builder->createField('filesystemId', Types::STRING), $emConfig)->columnName('filesystem_id')
fieldWithUtf8Charset($builder->createField('filesystemId', Types::STRING), $emConfig)
->columnName('filesystem_id')
->length(512)
->build();
@@ -15,7 +15,8 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->setTable('redirect_conditions');
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
@@ -26,16 +27,19 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
'fieldName' => 'type',
'type' => Types::STRING,
'enumType' => RedirectConditionType::class,
])->columnName('type')
])
->columnName('type')
->length(255)
->build();
fieldWithUtf8Charset($builder->createField('matchKey', Types::STRING), $emConfig)->columnName('match_key')
fieldWithUtf8Charset($builder->createField('matchKey', Types::STRING), $emConfig)
->columnName('match_key')
->length(512)
->nullable()
->build();
fieldWithUtf8Charset($builder->createField('matchValue', Types::STRING), $emConfig)->columnName('match_value')
fieldWithUtf8Charset($builder->createField('matchValue', Types::STRING), $emConfig)
->columnName('match_value')
->length(512)
->nullable()
->build();
@@ -13,28 +13,33 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->setTable('short_url_redirect_rules');
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
$builder->createField('priority', Types::INTEGER)
$builder
->createField('priority', Types::INTEGER)
->columnName('priority')
->build();
fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig)->columnName('long_url')
fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig)
->columnName('long_url')
->length(2048)
->build();
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
$builder
->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE')
->build();
// We treat this ManyToMany relation as a unidirectional OneToMany, where conditions are persisted and deleted
// together with the rule
$builder->createManyToMany('conditions', RedirectRule\Entity\RedirectCondition::class)
$builder
->createManyToMany('conditions', RedirectRule\Entity\RedirectCondition::class)
->setJoinTable('redirect_conditions_in_short_url_redirect_rules')
->addInverseJoinColumn('redirect_condition_id', 'id', onDelete: 'CASCADE')
->addJoinColumn('short_url_redirect_rule_id', 'id', onDelete: 'CASCADE')
@@ -13,17 +13,18 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('short_urls')
->setCustomRepositoryClass(ShortUrl\Repository\ShortUrlRepository::class);
$builder->setTable('short_urls')->setCustomRepositoryClass(ShortUrl\Repository\ShortUrlRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig)->columnName('original_url') // Rename to long_url some day? ¯\_(ツ)_/¯
fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig)
->columnName('original_url') // Rename to long_url some day? ¯\_(ツ)_/¯
->length(2048)
->build();
@@ -31,7 +32,8 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->createField('shortCode', Types::STRING),
$emConfig,
'bin',
)->columnName('short_code')
)
->columnName('short_code')
->length(255);
if (($emConfig['connection']['driver'] ?? null) === 'pdo_sqlsrv') {
// Make sure a case-sensitive charset is set in short code for Microsoft SQL Server
@@ -39,85 +41,99 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
}
$shortCodeField->build();
$builder->createField('dateCreated', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('dateCreated', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('date_created')
->build();
$builder->createField('validSince', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('validSince', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('valid_since')
->nullable()
->build();
$builder->createField('validUntil', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('validUntil', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('valid_until')
->nullable()
->build();
$builder->createField('maxVisits', Types::INTEGER)
$builder
->createField('maxVisits', Types::INTEGER)
->columnName('max_visits')
->nullable()
->build();
$builder->createField('importSource', Types::STRING)
$builder
->createField('importSource', Types::STRING)
->columnName('import_source')
->nullable()
->build();
fieldWithUtf8Charset($builder->createField('importOriginalShortCode', Types::STRING), $emConfig)->columnName(
'import_original_short_code',
)
fieldWithUtf8Charset($builder->createField('importOriginalShortCode', Types::STRING), $emConfig)
->columnName('import_original_short_code')
->nullable()
->build();
$builder->createOneToMany('visits', Visit\Entity\Visit::class)
$builder
->createOneToMany('visits', Visit\Entity\Visit::class)
->mappedBy('shortUrl')
->fetchExtraLazy()
->build();
$builder->createOneToMany('visitsCounts', Visit\Entity\ShortUrlVisitsCount::class)
$builder
->createOneToMany('visitsCounts', Visit\Entity\ShortUrlVisitsCount::class)
->mappedBy('shortUrl')
->fetchExtraLazy() // TODO Check if this makes sense
->build();
$builder->createManyToMany('tags', Tag\Entity\Tag::class)
$builder
->createManyToMany('tags', Tag\Entity\Tag::class)
->setJoinTable('short_urls_in_tags')
->addInverseJoinColumn('tag_id', 'id', onDelete: 'CASCADE')
->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE')
->setOrderBy(['name' => 'ASC'])
->build();
$builder->createManyToOne('domain', Domain\Entity\Domain::class)
$builder
->createManyToOne('domain', Domain\Entity\Domain::class)
->addJoinColumn('domain_id', 'id', onDelete: 'RESTRICT')
->cascadePersist()
->build();
$builder->createManyToOne('authorApiKey', ApiKey::class)
$builder
->createManyToOne('authorApiKey', ApiKey::class)
->addJoinColumn('author_api_key_id', 'id', onDelete: 'SET NULL')
->build();
$builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
fieldWithUtf8Charset($builder->createField('title', Types::STRING), $emConfig)->columnName('title')
fieldWithUtf8Charset($builder->createField('title', Types::STRING), $emConfig)
->columnName('title')
->length(512)
->nullable()
->build();
$builder->createField('titleWasAutoResolved', Types::BOOLEAN)
$builder
->createField('titleWasAutoResolved', Types::BOOLEAN)
->columnName('title_was_auto_resolved')
->option('default', false)
->build();
$builder->createField('crawlable', Types::BOOLEAN)
$builder
->createField('crawlable', Types::BOOLEAN)
->columnName('crawlable')
->option('default', false)
->build();
$builder->createField('forwardQuery', Types::BOOLEAN)
$builder
->createField('forwardQuery', Types::BOOLEAN)
->columnName('forward_query')
->option('default', true)
->build();
$builder->createOneToMany('redirectRules', RedirectRule\Entity\ShortUrlRedirectRule::class)
$builder
->createOneToMany('redirectRules', RedirectRule\Entity\ShortUrlRedirectRule::class)
->mappedBy('shortUrl')
->fetchExtraLazy()
->build();
@@ -11,18 +11,17 @@ use Doctrine\ORM\Mapping\ClassMetadata;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('tags')
->setCustomRepositoryClass(Tag\Repository\TagRepository::class);
$builder->setTable('tags')->setCustomRepositoryClass(Tag\Repository\TagRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
fieldWithUtf8Charset($builder->createField('name', Types::STRING), $emConfig)->unique()
->build();
fieldWithUtf8Charset($builder->createField('name', Types::STRING), $emConfig)->unique()->build();
$builder->addInverseManyToMany('shortUrls', ShortUrl\Entity\ShortUrl::class, 'tags');
};
@@ -11,28 +11,33 @@ use Doctrine\ORM\Mapping\ClassMetadata;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('orphan_visits_counts')
$builder
->setTable('orphan_visits_counts')
->setCustomRepositoryClass(Visit\Repository\OrphanVisitsCountRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
$builder->createField('potentialBot', Types::BOOLEAN)
$builder
->createField('potentialBot', Types::BOOLEAN)
->columnName('potential_bot')
->option('default', false)
->build();
$builder->createField('count', Types::BIGINT)
$builder
->createField('count', Types::BIGINT)
->columnName('count')
->option('unsigned', true)
->option('default', 1)
->build();
$builder->createField('slotId', Types::INTEGER)
$builder
->createField('slotId', Types::INTEGER)
->columnName('slot_id')
->option('unsigned', true)
->build();
@@ -11,33 +11,39 @@ use Doctrine\ORM\Mapping\ClassMetadata;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('short_url_visits_counts')
$builder
->setTable('short_url_visits_counts')
->setCustomRepositoryClass(Visit\Repository\ShortUrlVisitsCountRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
$builder->createField('potentialBot', Types::BOOLEAN)
$builder
->createField('potentialBot', Types::BOOLEAN)
->columnName('potential_bot')
->option('default', false)
->build();
$builder->createField('count', Types::BIGINT)
$builder
->createField('count', Types::BIGINT)
->columnName('count')
->option('unsigned', true)
->option('default', 1)
->build();
$builder->createField('slotId', Types::INTEGER)
$builder
->createField('slotId', Types::INTEGER)
->columnName('slot_id')
->option('unsigned', true)
->build();
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
$builder
->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE')
->build();
@@ -15,47 +15,54 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitType;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('visits')
->setCustomRepositoryClass(Visit\Repository\VisitRepository::class);
$builder->setTable('visits')->setCustomRepositoryClass(Visit\Repository\VisitRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
fieldWithUtf8Charset($builder->createField('referer', Types::STRING), $emConfig)->nullable()
fieldWithUtf8Charset($builder->createField('referer', Types::STRING), $emConfig)
->nullable()
->length(Visitor::REFERER_MAX_LENGTH)
->build();
$builder->createField('date', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('date', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('`date`')
->build();
$builder->addIndex(['date'], 'IDX_visits_date');
$builder->createField('remoteAddr', Types::STRING)
$builder
->createField('remoteAddr', Types::STRING)
->columnName('remote_addr')
->length(Visitor::REMOTE_ADDRESS_MAX_LENGTH)
->nullable()
->build();
fieldWithUtf8Charset($builder->createField('userAgent', Types::STRING), $emConfig)->columnName('user_agent')
fieldWithUtf8Charset($builder->createField('userAgent', Types::STRING), $emConfig)
->columnName('user_agent')
->length(Visitor::USER_AGENT_MAX_LENGTH)
->nullable()
->build();
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
$builder
->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE')
->build();
$builder->createManyToOne('visitLocation', Visit\Entity\VisitLocation::class)
$builder
->createManyToOne('visitLocation', Visit\Entity\VisitLocation::class)
->addJoinColumn('visit_location_id', 'id', onDelete: 'Set NULL')
->cascadePersist()
->build();
fieldWithUtf8Charset($builder->createField('visitedUrl', Types::STRING), $emConfig)->columnName('visited_url')
fieldWithUtf8Charset($builder->createField('visitedUrl', Types::STRING), $emConfig)
->columnName('visited_url')
->length(Visitor::VISITED_URL_MAX_LENGTH)
->nullable()
->build();
@@ -64,16 +71,19 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
'fieldName' => 'type',
'type' => Types::STRING,
'enumType' => VisitType::class,
])->columnName('type')
])
->columnName('type')
->length(255)
->build();
$builder->createField('potentialBot', Types::BOOLEAN)
$builder
->createField('potentialBot', Types::BOOLEAN)
->columnName('potential_bot')
->option('default', false)
->build();
fieldWithUtf8Charset($builder->createField('redirectUrl', Types::STRING), $emConfig)->columnName('redirect_url')
fieldWithUtf8Charset($builder->createField('redirectUrl', Types::STRING), $emConfig)
->columnName('redirect_url')
->length(Visitor::REDIRECT_URL_MAX_LENGTH)
->nullable()
->build();
@@ -13,7 +13,8 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->setTable('visit_locations');
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
@@ -29,22 +30,26 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
];
foreach ($columns as $columnName => $fieldName) {
fieldWithUtf8Charset($builder->createField($fieldName, Types::STRING), $emConfig)->columnName($columnName)
fieldWithUtf8Charset($builder->createField($fieldName, Types::STRING), $emConfig)
->columnName($columnName)
->nullable()
->build();
}
$builder->createField('latitude', Types::FLOAT)
$builder
->createField('latitude', Types::FLOAT)
->columnName('lat')
->nullable(false)
->build();
$builder->createField('longitude', Types::FLOAT)
$builder
->createField('longitude', Types::FLOAT)
->columnName('lon')
->nullable(false)
->build();
$builder->createField('isEmpty', Types::BOOLEAN)
$builder
->createField('isEmpty', Types::BOOLEAN)
->columnName('is_empty')
->option('default', false)
->nullable(false)
@@ -18,7 +18,8 @@ final class Version20240226214216 extends AbstractMigration
// Insert a rule per every device_long_url, and link it to the corresponding condition
$qb = $this->connection->createQueryBuilder();
$rules = $qb->select('short_url_id', 'device_type', 'long_url')
$rules = $qb
->select('short_url_id', 'device_type', 'long_url')
->from('device_long_urls')
->executeQuery();
@@ -28,7 +29,8 @@ final class Version20240226214216 extends AbstractMigration
$priority = $priorities[$shortUrlId] ?? 1;
$ruleQb = $this->connection->createQueryBuilder();
$ruleQb->insert('short_url_redirect_rules')
$ruleQb
->insert('short_url_redirect_rules')
->values([
'priority' => ':priority',
'long_url' => ':long_url',
@@ -44,7 +46,8 @@ final class Version20240226214216 extends AbstractMigration
$deviceType = $ruleRow['device_type'];
$conditionQb = $this->connection->createQueryBuilder();
$conditionQb->insert('redirect_conditions')
$conditionQb
->insert('redirect_conditions')
->values([
'type' => ':type',
'match_value' => ':match_value',
@@ -59,7 +62,8 @@ final class Version20240226214216 extends AbstractMigration
$conditionId = $this->connection->lastInsertId();
$relationQb = $this->connection->createQueryBuilder();
$relationQb->insert('redirect_conditions_in_short_url_redirect_rules')
$relationQb
->insert('redirect_conditions_in_short_url_redirect_rules')
->values([
'redirect_condition_id' => ':redirect_condition_id',
'short_url_redirect_rule_id' => ':short_url_redirect_rule_id',
@@ -15,13 +15,15 @@ final class Version20240318084804 extends AbstractMigration
public function up(Schema $schema): void
{
$qb = $this->connection->createQueryBuilder();
$result = $qb->select('id')
$result = $qb
->select('id')
->from('short_urls')
->executeQuery();
while ($shortUrlId = $result->fetchOne()) {
$visitsQb = $this->connection->createQueryBuilder();
$visitsQb->select('COUNT(id)')
$visitsQb
->select('COUNT(id)')
->from('visits')
->where($visitsQb->expr()->eq('short_url_id', ':short_url_id'))
->andWhere($visitsQb->expr()->eq('potential_bot', ':potential_bot'))
@@ -12,7 +12,8 @@ final class Version20240331111447 extends AbstractMigration
public function up(Schema $schema): void
{
$visitsQb = $this->connection->createQueryBuilder();
$visitsQb->select('COUNT(id)')
$visitsQb
->select('COUNT(id)')
->from('visits')
->where($visitsQb->expr()->isNull('short_url_id'))
->andWhere($visitsQb->expr()->eq('potential_bot', ':potential_bot'));
@@ -20,14 +20,16 @@ final class Version20241105094747 extends AbstractMigration
// Append key to the name for all API keys that already have a name
$qb = $this->connection->createQueryBuilder();
$qb->update('api_keys')
$qb
->update('api_keys')
->set('name', 'CONCAT(name, ' . $this->connection->quote(' - ') . ', ' . $keyColumnName . ')')
->where($qb->expr()->isNotNull('name'));
$qb->executeStatement();
// Set plain key as name for all API keys without a name
$qb = $this->connection->createQueryBuilder();
$qb->update('api_keys')
$qb
->update('api_keys')
->set('name', $keyColumnName)
->where($qb->expr()->isNull('name'));
$qb->executeStatement();
@@ -25,7 +25,8 @@ final class Version20241105215309 extends AbstractMigration
$result = $qb->executeQuery();
$updateQb = $this->connection->createQueryBuilder();
$updateQb->update('api_keys')
$updateQb
->update('api_keys')
->set($keyColumnName, ':encryptedKey')
->where($updateQb->expr()->eq($keyColumnName, ':plainTextKey'));
@@ -78,7 +78,8 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
$query,
);
return $redirectUri->withPath($replacePlaceholdersInPath($redirectUri->getPath()))
return $redirectUri
->withPath($replacePlaceholdersInPath($redirectUri->getPath()))
->withQuery($replacePlaceholdersInQuery($redirectUri->getQuery()))
->__toString();
}
@@ -23,7 +23,8 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
public function findDomains(ApiKey|null $apiKey = null): array
{
$qb = $this->createQueryBuilder('d');
$qb->leftJoin(ShortUrl::class, 's', Join::WITH, 's.domain = d')
$qb
->leftJoin(ShortUrl::class, 's', Join::WITH, 's.domain = d')
->groupBy('d')
->orderBy('d.authority', 'ASC')
->having($qb->expr()->gt('COUNT(s.id)', '0'))
@@ -58,7 +59,8 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
private function createDomainQueryBuilder(string $authority, ApiKey|null $apiKey): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Domain::class, 'd')
$qb
->from(Domain::class, 'd')
->leftJoin(ShortUrl::class, 's', Join::WITH, 's.domain = d')
->where($qb->expr()->eq('d.authority', ':authority'))
->setParameter('authority', $authority)
@@ -28,7 +28,8 @@ readonly class MatomoTrackerBuilder implements MatomoTrackerBuilderInterface
// Create a new MatomoTracker on every request, because it infers request info during construction
$tracker = new MatomoTracker($siteId, $this->options->baseUrl);
// Token required to set the IP and location
$tracker->setTokenAuth($this->options->apiToken)
$tracker
->setTokenAuth($this->options->apiToken)
// Ensure params are not sent in the URL, for security reasons
->setRequestMethodNonBulk('POST')
// Set a reasonable timeout
+4 -2
View File
@@ -50,7 +50,8 @@ readonly class MatomoVisitSender implements MatomoVisitSenderInterface
{
$tracker = $this->trackerBuilder->buildMatomoTracker();
$tracker->setUrl($this->resolveUrlToTrack($visit))
$tracker
->setUrl($this->resolveUrlToTrack($visit))
->setCustomTrackingParameter('type', $visit->type->value)
->setUserAgent($visit->userAgent)
->setUrlReferrer($visit->referer)
@@ -58,7 +59,8 @@ readonly class MatomoVisitSender implements MatomoVisitSenderInterface
$location = $visit->visitLocation;
if ($location !== null) {
$tracker->setCity($location->cityName)
$tracker
->setCity($location->cityName)
->setCountry(strtolower($location->countryCode))
->setLatitude($location->latitude)
->setLongitude($location->longitude);
@@ -36,9 +36,10 @@ readonly class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderI
// names are valid variable names.
$currentQuery = Query::parse($request->getUri()->getQuery());
return $uri->withQuery(
$shouldForwardQuery ? $this->resolveQuery($baseQueryString, $currentQuery) : $baseQueryString,
)
return $uri
->withQuery(
$shouldForwardQuery ? $this->resolveQuery($baseQueryString, $currentQuery) : $baseQueryString,
)
->withPath($this->resolvePath($basePath, $extraPath))
->__toString();
}
@@ -21,7 +21,8 @@ readonly class ShortUrlStringifier implements ShortUrlStringifierInterface
public function stringify(ShortUrl|ShortUrlIdentifier $shortUrl): string
{
$shortUrlIdentifier = $shortUrl instanceof ShortUrl ? ShortUrlIdentifier::fromShortUrl($shortUrl) : $shortUrl;
$uriWithoutShortCode = new Uri()->withScheme($this->urlShortenerOptions->schema)
$uriWithoutShortCode = new Uri()
->withScheme($this->urlShortenerOptions->schema)
->withHost($this->resolveDomain($shortUrlIdentifier))
->withPath($this->basePath)
->__toString();
@@ -17,7 +17,8 @@ class CrawlableShortCodesQuery extends EntitySpecificationRepository implements
{
$blockSize = 1000;
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('DISTINCT s.shortCode')
$qb
->select('DISTINCT s.shortCode')
->from(ShortUrl::class, 's')
->where($qb->expr()->eq('s.crawlable', ':crawlable'))
->setParameter('crawlable', true)
@@ -31,7 +31,8 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
{
$buildVisitsSubQuery = function (string $alias, bool $excludingBots): string {
$vqb = $this->getEntityManager()->createQueryBuilder();
$vqb->select('COALESCE(SUM(' . $alias . '.count), 0)')
$vqb
->select('COALESCE(SUM(' . $alias . '.count), 0)')
->from(ShortUrlVisitsCount::class, $alias)
->where($vqb->expr()->eq($alias . '.shortUrl', 's'));
@@ -43,13 +44,14 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
};
$qb = $this->createListQueryBuilder($filtering);
$qb->select(
'DISTINCT s AS shortUrl, d.authority',
'(' . $buildVisitsSubQuery('v', excludingBots: false) . ') AS ' . OrderableField::VISITS->value,
'(' . $buildVisitsSubQuery('v2', excludingBots: true) . ') AS ' . OrderableField::NON_BOT_VISITS->value,
// This is added only to have a consistent order by title between database engines
'COALESCE(s.title, \'\') AS title',
)
$qb
->select(
'DISTINCT s AS shortUrl, d.authority',
'(' . $buildVisitsSubQuery('v', excludingBots: false) . ') AS ' . OrderableField::VISITS->value,
'(' . $buildVisitsSubQuery('v2', excludingBots: true) . ') AS ' . OrderableField::NON_BOT_VISITS->value,
// This is added only to have a consistent order by title between database engines
'COALESCE(s.title, \'\') AS title',
)
->setMaxResults($filtering->limit)
->setFirstResult($filtering->offset)
// This param is used in one of the sub-queries, but needs to set in the parent query
@@ -90,7 +92,8 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
private function createListQueryBuilder(ShortUrlsCountFiltering $filtering): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(ShortUrl::class, 's')
$qb
->from(ShortUrl::class, 's')
->leftJoin('s.domain', 'd')
->where('1=1');
@@ -190,7 +193,8 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
$apiKeyName = $filtering->apiKeyName;
if ($apiKeyName !== null) {
$qb->join('s.authorApiKey', 'a')
$qb
->join('s.authorApiKey', 'a')
->andWhere($qb->expr()->eq('a.name', ':apiKeyName'))
->setParameter('apiKeyName', $apiKeyName);
}
@@ -35,7 +35,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
// Investigate if the case-insensitive check can be done natively by the DB engine.
$qb = $this->createQueryBuilder('s');
$qb->where($qb->expr()->eq($isStrict ? 's.shortCode' : 'LOWER(s.shortCode)', ':shortCode'))
$qb
->where($qb->expr()->eq($isStrict ? 's.shortCode' : 'LOWER(s.shortCode)', ':shortCode'))
->setParameter('shortCode', $isStrict ? $identifier->shortCode : strtolower($identifier->shortCode))
->setMaxResults(1);
@@ -44,7 +45,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
if ($domain === null) {
$qb->andWhere($qb->expr()->isNull('s.domain'));
} else {
$qb->leftJoin('s.domain', 'd')
$qb
->leftJoin('s.domain', 'd')
->andWhere($qb->expr()->orX(
$qb->expr()->isNull('s.domain'),
$qb->expr()->eq('d.authority', ':domain'),
@@ -100,7 +102,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
private function createFindOneQueryBuilder(ShortUrlIdentifier $identifier, Specification|null $spec): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(ShortUrl::class, 's')
$qb
->from(ShortUrl::class, 's')
->where($qb->expr()->isNotNull('s.shortCode'))
->andWhere($qb->expr()->eq('s.shortCode', ':slug'))
->setParameter('slug', $identifier->shortCode)
@@ -117,7 +120,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('s')
$qb
->select('s')
->from(ShortUrl::class, 's')
->where($qb->expr()->eq('s.longUrl', ':longUrl'))
->setParameter('longUrl', $creation->longUrl)
@@ -141,7 +145,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
->setParameter('validUntil', $creation->validUntil, ChronosDateTimeType::CHRONOS_DATETIME);
}
if ($creation->hasDomain()) {
$qb->join('s.domain', 'd')
$qb
->join('s.domain', 'd')
->andWhere($qb->expr()->eq('d.authority', ':domain'))
->setParameter('domain', $creation->domain);
}
@@ -161,7 +166,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
// If tags where provided, we need an extra join to see the amount of tags that every short URL has, so that we
// can discard those that also have more tags, making sure only those fully matching are included.
$qb->join('s.tags', 't')
$qb
->join('s.tags', 't')
->groupBy('s')
->having($qb->expr()->eq('COUNT(t.id)', ':tagsAmount'))
->setParameter('tagsAmount', $tagsAmount);
@@ -181,7 +187,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
public function findOneByImportedUrl(ImportedShlinkUrl $url): ShortUrl|null
{
$qb = $this->createQueryBuilder('s');
$qb->andWhere($qb->expr()->eq('s.importOriginalShortCode', ':shortCode'))
$qb
->andWhere($qb->expr()->eq('s.importOriginalShortCode', ':shortCode'))
->setParameter('shortCode', $url->shortCode)
->andWhere($qb->expr()->eq('s.importSource', ':importSource'))
->setParameter('importSource', $url->source->value)
@@ -195,7 +202,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
private function whereDomainIs(QueryBuilder $qb, string|null $domain): void
{
if ($domain !== null) {
$qb->join('s.domain', 'd')
$qb
->join('s.domain', 'd')
->andWhere($qb->expr()->eq('d.authority', ':authority'))
->setParameter('authority', $domain);
} else {
@@ -23,8 +23,7 @@ readonly class ShortUrlListService implements ShortUrlListServiceInterface
{
$defaultDomain = $this->urlShortenerOptions->defaultDomain;
$paginator = new Paginator(new ShortUrlRepositoryAdapter($this->repo, $params, $apiKey, $defaultDomain));
$paginator->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
$paginator->setMaxPerPage($params->itemsPerPage)->setCurrentPage($params->page);
return $paginator;
}
@@ -65,7 +65,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
// For non-restricted API keys, we'll return tags which are not linked to any short URL
$joiningMethod = !ApiKey::isShortUrlRestricted($apiKey) ? 'leftJoin' : 'join';
$tagsSubQb = $conn->createQueryBuilder();
$tagsSubQb->select('t.id AS tag_id', 't.name AS tag', 'COUNT(DISTINCT s.id) AS short_urls_count')
$tagsSubQb
->select('t.id AS tag_id', 't.name AS tag', 'COUNT(DISTINCT s.id) AS short_urls_count')
->from('tags', 't')
->groupBy('t.id', 't.name')
->{$joiningMethod}('t', 'short_urls_in_tags', 'st', $tagsSubQb->expr()->eq('st.tag_id', 't.id'))
@@ -81,14 +82,16 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$commonJoinCondition = $visitsSubQb->expr()->eq('sc.short_url_id', 'st.short_url_id');
$visitsJoin = !$excludeBots
? $commonJoinCondition
: $visitsSubQb->expr()
: $visitsSubQb
->expr()
->and(
$commonJoinCondition,
$visitsSubQb->expr()->eq('sc.potential_bot', $conn->quote('0')),
)
->__toString();
return $visitsSubQb->select('st.tag_id AS tag_id', 'SUM(sc.count) AS ' . $aggregateAlias)
return $visitsSubQb
->select('st.tag_id AS tag_id', 'SUM(sc.count) AS ' . $aggregateAlias)
->from('short_url_visits_counts', 'sc')
->join('sc', 'short_urls_in_tags', 'st', $visitsJoin)
->join('sc', 'short_urls', 's', $visitsSubQb->expr()->eq('sc.short_url_id', 's.id'))
@@ -105,12 +108,13 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
// sub-queries at "from" and "join" level.
// If no sub-query is used, the whole list is loaded even with pagination, making it very inefficient.
$mainQb = $conn->createQueryBuilder();
$mainQb->select(
't.tag AS tag',
'COALESCE(v.visits, 0) AS visits', // COALESCE required for postgres to properly order
'COALESCE(b.non_bot_visits, 0) AS non_bot_visits',
'COALESCE(t.short_urls_count, 0) AS short_urls_count',
)
$mainQb
->select(
't.tag AS tag',
'COALESCE(v.visits, 0) AS visits', // COALESCE required for postgres to properly order
'COALESCE(b.non_bot_visits, 0) AS non_bot_visits',
'COALESCE(t.short_urls_count, 0) AS short_urls_count',
)
->from('(' . $tagsSubQb->getSQL() . ')', 't')
->leftJoin('t', '(' . $allVisitsSubQb->getSQL() . ')', 'v', $mainQb->expr()->eq('t.tag_id', 'v.tag_id'))
->leftJoin('t', '(' . $nonBotVisitsSubQb->getSQL() . ')', 'b', $mainQb->expr()->eq('t.tag_id', 'b.tag_id'))
+1 -2
View File
@@ -42,8 +42,7 @@ readonly class TagService implements TagServiceInterface
private function createPaginator(AdapterInterface $adapter, TagsParams $params): Paginator
{
$paginator = new Paginator($adapter);
$paginator->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
$paginator->setMaxPerPage($params->itemsPerPage)->setCurrentPage($params->page);
return $paginator;
}
@@ -110,7 +110,8 @@ final class OrphanVisitsCountTracker
// For engines without a specific UPSERT syntax, do a regular locked select followed by an insert or update
$qb = $conn->createQueryBuilder();
$qb->select('id')
$qb
->select('id')
->from('orphan_visits_counts')
->where($qb->expr()->and(
$qb->expr()->eq('potential_bot', ':potential_bot'),
@@ -127,7 +128,8 @@ final class OrphanVisitsCountTracker
$visitsCountId = $qb->executeQuery()->fetchOne();
$writeQb = !$visitsCountId
? $conn->createQueryBuilder()
? $conn
->createQueryBuilder()
->insert('orphan_visits_counts')
->values([
'potential_bot' => ':potential_bot',
@@ -135,7 +137,8 @@ final class OrphanVisitsCountTracker
])
->setParameter('potential_bot', $potentialBot ? '1' : '0')
->setParameter('slot_id', $slotId)
: $conn->createQueryBuilder()
: $conn
->createQueryBuilder()
->update('orphan_visits_counts')
->set('count', 'count + 1')
->where($qb->expr()->eq('id', ':visits_count_id'))
@@ -121,7 +121,8 @@ final class ShortUrlVisitsCountTracker
// For engines without a specific UPSERT syntax, do a regular locked select followed by an insert or update
$qb = $conn->createQueryBuilder();
$qb->select('id')
$qb
->select('id')
->from('short_url_visits_counts')
->where($qb->expr()->and(
$qb->expr()->eq('short_url_id', ':short_url_id'),
@@ -140,7 +141,8 @@ final class ShortUrlVisitsCountTracker
$visitsCountId = $qb->executeQuery()->fetchOne();
$writeQb = !$visitsCountId
? $conn->createQueryBuilder()
? $conn
->createQueryBuilder()
->insert('short_url_visits_counts')
->values([
'short_url_id' => ':short_url_id',
@@ -150,7 +152,8 @@ final class ShortUrlVisitsCountTracker
->setParameter('short_url_id', $shortUrlId)
->setParameter('potential_bot', $potentialBot ? '1' : '0')
->setParameter('slot_id', $slotId)
: $conn->createQueryBuilder()
: $conn
->createQueryBuilder()
->update('short_url_visits_counts')
->set('count', 'count + 1')
->where($qb->expr()->eq('id', ':visits_count_id'))
@@ -16,7 +16,8 @@ class ShortUrlVisitsCountRepository extends EntitySpecificationRepository implem
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('COALESCE(SUM(vc.count), 0)')
$qb
->select('COALESCE(SUM(vc.count), 0)')
->from(ShortUrlVisitsCount::class, 'vc')
->join('vc.shortUrl', 's');
@@ -27,7 +27,8 @@ class VisitDeleterRepository extends EntitySpecificationRepository implements Vi
private function deleteByShortUrl(string $entityName, ShortUrl $shortUrl): int
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->delete($entityName, 'v')
$qb
->delete($entityName, 'v')
->where($qb->expr()->eq('v.shortUrl', ':shortUrl'))
->setParameter('shortUrl', $shortUrl);
@@ -22,7 +22,8 @@ class VisitIterationRepository extends EntitySpecificationRepository implements
public function findUnlocatedVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('v')
$qb
->select('v')
->from(Visit::class, 'v')
->where($qb->expr()->isNull('v.visitLocation'));
@@ -35,7 +36,8 @@ class VisitIterationRepository extends EntitySpecificationRepository implements
public function findVisitsWithEmptyLocation(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('v')
$qb
->select('v')
->from(Visit::class, 'v')
->join('v.visitLocation', 'vl')
->where($qb->expr()->isNotNull('v.visitLocation'))
@@ -89,7 +89,8 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later.
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v')
$qb
->from(Visit::class, 'v')
->join('v.shortUrl', 's')
->join('s.tags', 't')
->where($qb->expr()->eq('t.name', $conn->quote($tag)));
@@ -270,7 +271,8 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
// TODO Order by date and ID, not just by ID (order by date DESC, id DESC).
// That ensures imported visits are properly ordered even if inserted in wrong chronological order.
$qb->select('v.id')
$qb
->select('v.id')
->orderBy('v.id', 'DESC')
// Falling back to values that will behave as no limit/offset, but will work around MS SQL not allowing
// order on sub-queries without offset
@@ -282,7 +284,8 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
// sub-queries at "from" and "join" level.
// If no sub-query is used, then performance drops dramatically while the "offset" grows.
$nativeQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
$nativeQb->select('v.id AS visit_id', 'v.*', 'vl.*')
$nativeQb
->select('v.id AS visit_id', 'v.*', 'vl.*')
->from('visits', 'v')
// @phpstan-ignore-next-line
->join('v', '(' . $subQuery . ')', 'sq', $nativeQb->expr()->eq('sq.id_0', 'v.id'))
+1 -2
View File
@@ -140,8 +140,7 @@ readonly class VisitsStatsHelper implements VisitsStatsHelperInterface
private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator
{
$paginator = new Paginator($adapter);
$paginator->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
$paginator->setMaxPerPage($params->itemsPerPage)->setCurrentPage($params->page);
return $paginator;
}
@@ -68,13 +68,15 @@ class NotFoundRedirectHandlerTest extends TestCase
MockObject&DomainServiceInterface $domainService,
MockObject&NotFoundRedirectResolverInterface $resolver,
) use ($once): void {
$domainService->expects($once())
$domainService
->expects($once())
->method('findByAuthority')
->withAnyParameters()
->willReturn(
null,
);
$resolver->expects($once())
$resolver
->expects($once())
->method('resolveRedirectResponse')
->with(
self::isInstanceOf(NotFoundType::class),
@@ -87,14 +89,16 @@ class NotFoundRedirectHandlerTest extends TestCase
MockObject&DomainServiceInterface $domainService,
MockObject&NotFoundRedirectResolverInterface $resolver,
) use ($once, $exactly): void {
$domainService->expects($once())
$domainService
->expects($once())
->method('findByAuthority')
->withAnyParameters()
->willReturn(
Domain::withAuthority(''),
);
$callCount = 0;
$resolver->expects($exactly(2))
$resolver
->expects($exactly(2))
->method('resolveRedirectResponse')
->willReturnCallback(
static function (mixed $arg1, mixed $arg2, mixed $arg3) use (&$callCount) {
@@ -185,7 +185,8 @@ class NotifyVisitToRabbitMqTest extends TestCase
): void {
$update = Update::forTopicAndPayload('', []);
$updatesGenerator->expects($never())->method('newOrphanVisitUpdate');
$updatesGenerator->expects($once())
$updatesGenerator
->expects($once())
->method('newVisitUpdate')
->withAnyParameters()
->willReturn(
@@ -382,7 +382,8 @@ class ImportedLinksProcessorTest extends TestCase
);
$visitRepo = $this->createMock(VisitRepository::class);
$visitRepo->expects($importOrphanVisits ? $this->once() : $this->never())
$visitRepo
->expects($importOrphanVisits ? $this->once() : $this->never())
->method(
'findMostRecentOrphanVisit',
)
@@ -54,24 +54,28 @@ class MatomoVisitSenderTest extends TestCase
$tracker->expects($this->once())->method('setUrl')->willReturn($tracker);
$tracker->expects($this->once())->method('setUserAgent')->willReturn($tracker);
$tracker->expects($this->once())->method('setUrlReferrer')->willReturn($tracker);
$tracker->expects($this->once())
$tracker
->expects($this->once())
->method('doTrackPageView')
->with($visit->shortUrl?->title() ?? '');
$tracker->expects($this->once())
$tracker
->expects($this->once())
->method('setForceVisitDateTime')
->with(
$visit->date->setTimezone('UTC')->toDateTimeString(),
);
if ($visit->isOrphan()) {
$tracker->expects($this->exactly(2))
$tracker
->expects($this->exactly(2))
->method('setCustomTrackingParameter')
->willReturnMap([
['type', $visit->type->value, $tracker],
['orphan', 'true', $tracker],
]);
} else {
$tracker->expects($this->once())
$tracker
->expects($this->once())
->method('setCustomTrackingParameter')
->with(
'type',
@@ -81,7 +85,8 @@ class MatomoVisitSenderTest extends TestCase
}
foreach ($invokedMethods as $invokedMethod => $args) {
$tracker->expects($this->once())
$tracker
->expects($this->once())
->method($invokedMethod)
->with(...array_values($args))
->willReturn(
@@ -46,7 +46,8 @@ class ShortUrlRedirectRuleServiceTest extends TestCase
];
$repo = $this->createMock(EntityRepository::class);
$repo->expects($this->once())
$repo
->expects($this->once())
->method('findBy')
->with(
['shortUrl' => $shortUrl],
@@ -107,7 +108,8 @@ class ShortUrlRedirectRuleServiceTest extends TestCase
$data = new RedirectRulesData([]);
$repo = $this->createMock(EntityRepository::class);
$repo->expects($this->once())
$repo
->expects($this->once())
->method('findBy')
->with(
['shortUrl' => $shortUrl],
@@ -77,7 +77,8 @@ class PersistenceShortUrlRelationResolverTest extends TestCase
$expectedPersistedTags = $expectedLookedOutTags - 1;
$tagRepo = $this->createMock(TagRepository::class);
$tagRepo->expects($this->exactly($expectedLookedOutTags))
$tagRepo
->expects($this->exactly($expectedLookedOutTags))
->method('findOneBy')
->with(
$this->isArray(),
@@ -63,7 +63,8 @@ class VisitsStatsHelperTest extends TestCase
{
$callCount = 0;
$visitsCountRepo = $this->createMock(ShortUrlVisitsCountRepository::class);
$visitsCountRepo->expects($this->exactly(2))
$visitsCountRepo
->expects($this->exactly(2))
->method('countNonOrphanVisits')
->willReturnCallback(
static function (VisitsCountFiltering $options) use ($expectedCount, $apiKey, &$callCount) {
@@ -76,7 +77,8 @@ class VisitsStatsHelperTest extends TestCase
);
$orphanVisitsCountRepo = $this->createMock(OrphanVisitsCountRepository::class);
$orphanVisitsCountRepo->expects($this->exactly(2))
$orphanVisitsCountRepo
->expects($this->exactly(2))
->method('countOrphanVisits')
->with(
$this->isInstanceOf(VisitsCountFiltering::class),
@@ -266,13 +268,15 @@ class VisitsStatsHelperTest extends TestCase
{
$list = array_map(static fn () => Visit::forBasePath(Visitor::empty()), range(0, 3));
$repo = $this->createMock(VisitRepository::class);
$repo->expects($this->once())
$repo
->expects($this->once())
->method('countOrphanVisits')
->with(
$this->isInstanceOf(OrphanVisitsCountFiltering::class),
)
->willReturn(count($list));
$repo->expects($this->once())
$repo
->expects($this->once())
->method('findOrphanVisits')
->with(
$this->isInstanceOf(OrphanVisitsListFiltering::class),
@@ -293,13 +297,15 @@ class VisitsStatsHelperTest extends TestCase
range(0, 3),
);
$repo = $this->createMock(VisitRepository::class);
$repo->expects($this->once())
$repo
->expects($this->once())
->method('countNonOrphanVisits')
->with(
$this->isInstanceOf(WithDOmainVisitsCountFiltering::class),
)
->willReturn(count($list));
$repo->expects($this->once())
$repo
->expects($this->once())
->method('findNonOrphanVisits')
->with(
$this->isInstanceOf(WithDOmainVisitsListFiltering::class),
@@ -13,34 +13,37 @@ use Shlinkio\Shlink\Rest\Entity\ApiKeyRole;
return static function (ClassMetadata $metadata): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('api_keys')
->setCustomRepositoryClass(ApiKey\Repository\ApiKeyRepository::class);
$builder->setTable('api_keys')->setCustomRepositoryClass(ApiKey\Repository\ApiKeyRepository::class);
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
$builder->createField('key', Types::STRING)
$builder
->createField('key', Types::STRING)
->columnName('`key`')
->unique()
->build();
$builder->createField('name', Types::STRING)
$builder
->createField('name', Types::STRING)
->columnName('`name`')
->nullable()
->build();
$builder->createField('expirationDate', ChronosDateTimeType::CHRONOS_DATETIME)
$builder
->createField('expirationDate', ChronosDateTimeType::CHRONOS_DATETIME)
->columnName('expiration_date')
->nullable()
->build();
$builder->createField('enabled', Types::BOOLEAN)
->build();
$builder->createField('enabled', Types::BOOLEAN)->build();
$builder->createOneToMany('roles', ApiKeyRole::class)
$builder
->createOneToMany('roles', ApiKeyRole::class)
->mappedBy('apiKey')
->setIndexBy('role')
->cascadePersist()
@@ -16,7 +16,8 @@ return static function (ClassMetadata $metadata): void {
$builder->setTable('api_key_roles');
$builder->createField('id', Types::BIGINT)
$builder
->createField('id', Types::BIGINT)
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
@@ -26,17 +27,20 @@ return static function (ClassMetadata $metadata): void {
'fieldName' => 'role',
'type' => Types::STRING,
'enumType' => Role::class,
])->columnName('role_name')
])
->columnName('role_name')
->length(255)
->nullable(false)
->build();
$builder->createField('meta', Types::JSON)
$builder
->createField('meta', Types::JSON)
->columnName('meta')
->nullable(false)
->build();
$builder->createManyToOne('apiKey', ApiKey::class)
$builder
->createManyToOne('apiKey', ApiKey::class)
->addJoinColumn('api_key_id', 'id', false, false, 'CASCADE')
->cascadePersist()
->build();
@@ -20,7 +20,8 @@ class ApiKeyRepository extends EntitySpecificationRepository implements ApiKeyRe
{
$em = $this->getEntityManager();
return $em->wrapInTransaction(static function () use ($apiKey, $em): ApiKey|null {
$firstResult = $em->createQueryBuilder()
$firstResult = $em
->createQueryBuilder()
->select('a.id')
->from(ApiKey::class, 'a')
->setMaxResults(1)
@@ -44,7 +45,8 @@ class ApiKeyRepository extends EntitySpecificationRepository implements ApiKeyRe
public function nameExists(string $name): bool
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('a.id')
$qb
->select('a.id')
->from(ApiKey::class, 'a')
->setMaxResults(1);
@@ -18,7 +18,8 @@ readonly class DropDefaultDomainFromRequestMiddleware implements MiddlewareInter
{
/** @var array $body */
$body = $request->getParsedBody() ?? [];
$request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams()))
$request = $request
->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams()))
->withParsedBody($this->sanitizeDomainFromPayload($body));
return $handler->handle($request);
@@ -46,7 +46,8 @@ class ResolveShortUrlActionTest extends TestCase
)
->willReturn(ShortUrl::withLongUrl('http://domain.com/foo/bar'));
$request = new ServerRequest()->withAttribute('shortCode', $shortCode)
$request = new ServerRequest()
->withAttribute('shortCode', $shortCode)
->withAttribute(ApiKey::class, $apiKey);
$response = $this->action->handle($request);
@@ -67,7 +67,8 @@ class ShortUrlVisitsActionTest extends TestCase
->willReturn(new Paginator(new ArrayAdapter([])));
$response = $this->action->handle(
$this->requestWithApiKey()
$this
->requestWithApiKey()
->withAttribute('shortCode', $shortCode)
->withQueryParams([
'endDate' => '2016-01-01 00:00:00',
@@ -68,10 +68,11 @@ class AuthenticationMiddlewareTest extends TestCase
new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, HealthAction::class),
),
)];
yield 'OPTIONS method' => [new ServerRequest()->withAttribute(
RouteResult::class,
RouteResult::fromRoute(new Route('bar', $dummyMiddleware), []),
)
yield 'OPTIONS method' => [new ServerRequest()
->withAttribute(
RouteResult::class,
RouteResult::fromRoute(new Route('bar', $dummyMiddleware), []),
)
->withMethod(RequestMethodInterface::METHOD_OPTIONS)];
}
@@ -68,10 +68,12 @@ class BodyParserMiddlewareTest extends TestCase
{
$body = new Stream('php://temp', 'wr');
$body->write('{"foo": "bar", "bar": ["one", 5]}');
$request = new ServerRequest()->withMethod('PUT')
$request = new ServerRequest()
->withMethod('PUT')
->withBody($body);
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects($this->once())
$handler
->expects($this->once())
->method('handle')
->with(
$this->isInstanceOf(ServerRequestInterface::class),
@@ -93,7 +95,8 @@ class BodyParserMiddlewareTest extends TestCase
{
$body = new Stream('php://temp', 'wr');
$body->write('{"foo": "bar", "bar": ["one');
$request = new ServerRequest()->withMethod('PUT')
$request = new ServerRequest()
->withMethod('PUT')
->withBody($body);
$handler = $this->createMock(RequestHandlerInterface::class);
@@ -63,7 +63,8 @@ class CrossDomainMiddlewareTest extends TestCase
public function optionsRequestIncludesMoreHeaders(): void
{
$originalResponse = new Response();
$request = new ServerRequest()->withMethod('OPTIONS')
$request = new ServerRequest()
->withMethod('OPTIONS')
->withHeader('Origin', 'local')
->withHeader('Access-Control-Request-Headers', 'foo, bar, baz');
$this->handler->expects($this->once())->method('handle')->willReturn($originalResponse);
@@ -89,7 +90,8 @@ class CrossDomainMiddlewareTest extends TestCase
if ($allowHeader !== null) {
$originalResponse = $originalResponse->withHeader('Allow', $allowHeader);
}
$request = new ServerRequest()->withHeader('Origin', 'local')
$request = new ServerRequest()
->withHeader('Origin', 'local')
->withMethod('OPTIONS');
$this->handler->expects($this->once())->method('handle')->willReturn($originalResponse);
@@ -113,7 +115,8 @@ class CrossDomainMiddlewareTest extends TestCase
int $expectedStatus,
): void {
$originalResponse = new Response()->withStatus($status);
$request = new ServerRequest()->withMethod($method)
$request = new ServerRequest()
->withMethod($method)
->withHeader('Origin', 'local');
$this->handler->expects($this->once())->method('handle')->willReturn($originalResponse);
@@ -151,7 +154,8 @@ class CrossDomainMiddlewareTest extends TestCase
public function credentialsAreAllowedIfConfiguredSo(bool $allowCredentials, string $method): void
{
$originalResponse = new Response();
$request = new ServerRequest()->withMethod($method)
$request = new ServerRequest()
->withMethod($method)
->withHeader('Origin', 'local');
$this->handler->expects($this->once())->method('handle')->willReturn($originalResponse);
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0"?>
<ruleset name="Coding standard">
<description>Coding standard</description>
<!-- display progress -->
<arg value="p" />
<arg name="colors" />
<!-- inherit rules from: -->
<rule ref="Shlinkio" />
<!-- Paths to check -->
<file>bin</file>
<file>module</file>
<file>config</file>
<file>docker/config</file>
<file>public/index.php</file>
<!-- Paths to exclude -->
<exclude-pattern>config/params/*</exclude-pattern>
</ruleset>