mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-20 09:19:54 +03:00
commit
a63075eb4c
65 changed files with 484 additions and 282 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -4,6 +4,24 @@ 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).
|
||||
|
||||
## [3.7.1] - 2023-12-17
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* Remove dependency on functional-php library
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1947](https://github.com/shlinkio/shlink/issues/1947) Fix error when importing short URLs while using Postgres.
|
||||
* [#1939](https://github.com/shlinkio/shlink/issues/1939) Fine-tune RoadRunner logs to avoid too many useless info.
|
||||
|
||||
|
||||
## [3.7.0] - 2023-11-25
|
||||
### Added
|
||||
* [#1798](https://github.com/shlinkio/shlink/issues/1798) Experimental support to send visits to an external Matomo instance.
|
||||
|
|
|
@ -23,18 +23,17 @@
|
|||
"doctrine/orm": "^2.16",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^2.13",
|
||||
"geoip2/geoip2": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"happyr/doctrine-specification": "^2.0",
|
||||
"jaybizzle/crawler-detect": "^1.2.116",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"laminas/laminas-config-aggregator": "^1.13",
|
||||
"laminas/laminas-diactoros": "^2.25",
|
||||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-inputfilter": "^2.27",
|
||||
"laminas/laminas-servicemanager": "^3.21",
|
||||
"laminas/laminas-stdlib": "^3.17",
|
||||
"league/uri": "^6.8",
|
||||
"lstrojny/functional-php": "^1.17",
|
||||
"matomo/matomo-php-tracker": "^3.2",
|
||||
"mezzio/mezzio": "^3.17",
|
||||
"mezzio/mezzio-fastroute": "^3.10",
|
||||
|
@ -46,12 +45,12 @@
|
|||
"php-middleware/request-id": "^4.1",
|
||||
"pugx/shortid-php": "^1.1",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"shlinkio/shlink-common": "^5.7",
|
||||
"shlinkio/shlink-common": "^5.7.1",
|
||||
"shlinkio/shlink-config": "^2.5",
|
||||
"shlinkio/shlink-event-dispatcher": "^3.1",
|
||||
"shlinkio/shlink-importer": "^5.2",
|
||||
"shlinkio/shlink-installer": "^8.6",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.3",
|
||||
"shlinkio/shlink-importer": "^5.2.1",
|
||||
"shlinkio/shlink-installer": "^8.6.1",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.4",
|
||||
"shlinkio/shlink-json": "^1.1",
|
||||
"spiral/roadrunner": "^2023.2",
|
||||
"spiral/roadrunner-cli": "^2.5",
|
||||
|
@ -80,6 +79,9 @@
|
|||
"symfony/var-dumper": "^6.3",
|
||||
"veewee/composer-run-parallel": "^1.3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/var-exporter": ">=6.3.9,<=6.4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||
|
@ -88,6 +90,7 @@
|
|||
},
|
||||
"files": [
|
||||
"config/constants.php",
|
||||
"module/Core/functions/array-utils.php",
|
||||
"module/Core/functions/functions.php"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -5,11 +5,11 @@ declare(strict_types=1);
|
|||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
return (static function (): array {
|
||||
$driver = EnvVars::DB_DRIVER->loadFromEnv();
|
||||
$isMysqlCompatible = contains(['maria', 'mysql'], $driver);
|
||||
$isMysqlCompatible = contains($driver, ['maria', 'mysql']);
|
||||
|
||||
$resolveDriver = static fn () => match ($driver) {
|
||||
'postgres' => 'pdo_pgsql',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '3.0'
|
||||
version: '3'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
@ -38,3 +38,5 @@ logs:
|
|||
level: debug
|
||||
metrics:
|
||||
level: debug
|
||||
jobs:
|
||||
level: debug
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '3.0'
|
||||
version: '3'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
@ -33,4 +33,6 @@ logs:
|
|||
http:
|
||||
mode: 'off' # Disable logging as Shlink handles it internally
|
||||
server:
|
||||
level: debug # Everything written to worker stderr is logged
|
||||
level: info
|
||||
jobs:
|
||||
level: debug
|
||||
|
|
|
@ -28,9 +28,9 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
|||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function Functional\contains;
|
||||
use function Laminas\Stratigility\middleware;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
|
@ -41,7 +41,7 @@ $isApiTest = env('TEST_ENV') === 'api';
|
|||
$isCliTest = env('TEST_ENV') === 'cli';
|
||||
$isE2eTest = $isApiTest || $isCliTest;
|
||||
$coverageType = env('GENERATE_COVERAGE');
|
||||
$generateCoverage = contains(['yes', 'pretty'], $coverageType);
|
||||
$generateCoverage = contains($coverageType, ['yes', 'pretty']);
|
||||
|
||||
$coverage = null;
|
||||
if ($isE2eTest && $generateCoverage) {
|
||||
|
|
|
@ -11,7 +11,7 @@ use Doctrine\DBAL\Schema\Schema;
|
|||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\some;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
|
||||
final class Version20200105165647 extends AbstractMigration
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ final class Version20200105165647 extends AbstractMigration
|
|||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(some(
|
||||
self::COLUMNS,
|
||||
fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName),
|
||||
fn (string $v, string|int $newColName) => $visitLocations->hasColumn((string) $newColName),
|
||||
), 'New columns already exist');
|
||||
|
||||
foreach (self::COLUMNS as $columnName) {
|
||||
|
|
|
@ -7,11 +7,10 @@ namespace ShlinkMigrations;
|
|||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\none;
|
||||
|
||||
final class Version20200106215144 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = ['latitude', 'longitude'];
|
||||
|
@ -22,16 +21,24 @@ final class Version20200106215144 extends AbstractMigration
|
|||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(none(
|
||||
self::COLUMNS,
|
||||
fn (string $oldColName) => $visitLocations->hasColumn($oldColName),
|
||||
), 'Old columns do not exist');
|
||||
$this->skipIf($this->oldColumnsDoNotExist($visitLocations), 'Old columns do not exist');
|
||||
|
||||
foreach (self::COLUMNS as $colName) {
|
||||
$visitLocations->dropColumn($colName);
|
||||
}
|
||||
}
|
||||
|
||||
public function oldColumnsDoNotExist(Table $visitLocations): bool
|
||||
{
|
||||
foreach (self::COLUMNS as $oldColName) {
|
||||
if ($visitLocations->hasColumn($oldColName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
|
|
|
@ -9,9 +9,6 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
|
|||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\each;
|
||||
use function Functional\partial_left;
|
||||
|
||||
final class Version20200110182849 extends AbstractMigration
|
||||
{
|
||||
private const DEFAULT_EMPTY_VALUE = '';
|
||||
|
@ -31,11 +28,11 @@ final class Version20200110182849 extends AbstractMigration
|
|||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
each(
|
||||
self::COLUMN_DEFAULTS_MAP,
|
||||
fn (array $columns, string $tableName) =>
|
||||
each($columns, partial_left([$this, 'setDefaultValueForColumnInTable'], $tableName)),
|
||||
);
|
||||
foreach (self::COLUMN_DEFAULTS_MAP as $tableName => $columns) {
|
||||
foreach ($columns as $columnName) {
|
||||
$this->setDefaultValueForColumnInTable($tableName, $columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -199,7 +199,7 @@ services:
|
|||
|
||||
shlink_swagger_ui:
|
||||
container_name: shlink_swagger_ui
|
||||
image: swaggerapi/swagger-ui:v5.9.1
|
||||
image: swaggerapi/swagger-ui:v5.10.3
|
||||
ports:
|
||||
- "8005:8080"
|
||||
volumes:
|
||||
|
|
|
@ -15,7 +15,7 @@ use Symfony\Component\Console\Input\InputOption;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_filter;
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -49,7 +49,7 @@ class ListKeysCommand extends Command
|
|||
{
|
||||
$enabledOnly = $input->getOption('enabled-only');
|
||||
|
||||
$rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$expiration = $apiKey->getExpirationDate();
|
||||
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||
|
||||
|
@ -64,7 +64,7 @@ class ListKeysCommand extends Command
|
|||
));
|
||||
|
||||
return $rowData;
|
||||
});
|
||||
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||
|
||||
ShlinkTable::withRowSeparators($output)->render(array_filter([
|
||||
'Key',
|
||||
|
|
|
@ -16,9 +16,9 @@ use Symfony\Component\Lock\LockFactory;
|
|||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Functional\map;
|
||||
use function Functional\some;
|
||||
use function array_map;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
|
||||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
|
@ -70,11 +70,11 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||
{
|
||||
$existingTables = $this->ensureDatabaseExistsAndGetTables();
|
||||
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
|
||||
$shlinkTables = map($allMetadata, static fn (ClassMetadata $metadata) => $metadata->getTableName());
|
||||
$shlinkTables = array_map(static fn (ClassMetadata $metadata) => $metadata->getTableName(), $allMetadata);
|
||||
|
||||
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
||||
// Any other inconsistency will be taken care of by the migrations.
|
||||
return some($shlinkTables, static fn (string $shlinkTable) => contains($existingTables, $shlinkTable));
|
||||
return some($shlinkTables, static fn (string $shlinkTable) => contains($shlinkTable, $existingTables));
|
||||
}
|
||||
|
||||
private function ensureDatabaseExistsAndGetTables(): array
|
||||
|
|
|
@ -14,8 +14,8 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function Functional\filter;
|
||||
use function Functional\invoke;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
|
@ -23,7 +23,7 @@ class DomainRedirectsCommand extends Command
|
|||
{
|
||||
public const NAME = 'domain:redirects';
|
||||
|
||||
public function __construct(private DomainServiceInterface $domainService)
|
||||
public function __construct(private readonly DomainServiceInterface $domainService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -52,9 +52,9 @@ class DomainRedirectsCommand extends Command
|
|||
$askNewDomain = static fn () => $io->ask('Domain authority for which you want to set specific redirects');
|
||||
|
||||
/** @var string[] $availableDomains */
|
||||
$availableDomains = invoke(
|
||||
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
|
||||
'toString',
|
||||
$availableDomains = array_map(
|
||||
static fn (DomainItem $item) => $item->toString(),
|
||||
array_filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
|
||||
);
|
||||
if (empty($availableDomains)) {
|
||||
$input->setArgument('domain', $askNewDomain());
|
||||
|
|
|
@ -14,13 +14,13 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class ListDomainsCommand extends Command
|
||||
{
|
||||
public const NAME = 'domain:list';
|
||||
|
||||
public function __construct(private DomainServiceInterface $domainService)
|
||||
public function __construct(private readonly DomainServiceInterface $domainService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class ListDomainsCommand extends Command
|
|||
|
||||
$table->render(
|
||||
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
|
||||
map($domains, function (DomainItem $domain) use ($showRedirects) {
|
||||
array_map(function (DomainItem $domain) use ($showRedirects) {
|
||||
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
|
||||
|
||||
return $showRedirects
|
||||
|
@ -56,7 +56,7 @@ class ListDomainsCommand extends Command
|
|||
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
|
||||
]
|
||||
: $commonValues;
|
||||
}),
|
||||
}, $domains),
|
||||
);
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
|
|
|
@ -20,10 +20,9 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function explode;
|
||||
use function Functional\curry;
|
||||
use function Functional\flatten;
|
||||
use function Functional\unique;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
|
||||
use function sprintf;
|
||||
|
||||
class CreateShortUrlCommand extends Command
|
||||
|
@ -144,8 +143,8 @@ class CreateShortUrlCommand extends Command
|
|||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
|
||||
$explodeWithComma = curry(explode(...))(',');
|
||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$explodeWithComma = static fn (string $tag) => explode(',', $tag);
|
||||
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $input->getOption('custom-slug');
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
|
||||
|
|
|
@ -23,9 +23,9 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function Functional\map;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -184,10 +184,10 @@ class ListShortUrlsCommand extends Command
|
|||
): Paginator {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
$rows = map($shortUrls, function (ShortUrl $shortUrl) use ($columnsMap) {
|
||||
$rows = array_map(function (ShortUrl $shortUrl) use ($columnsMap) {
|
||||
$rawShortUrl = $this->transformer->transform($shortUrl);
|
||||
return map($columnsMap, fn (callable $call) => $call($rawShortUrl, $shortUrl));
|
||||
});
|
||||
return array_map(fn (callable $call) => $call($rawShortUrl, $shortUrl), $columnsMap);
|
||||
}, [...$shortUrls]);
|
||||
|
||||
ShlinkTable::default($output)->render(
|
||||
array_keys($columnsMap),
|
||||
|
|
|
@ -13,13 +13,13 @@ use Symfony\Component\Console\Command\Command;
|
|||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
public function __construct(private readonly TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -44,9 +44,9 @@ class ListTagsCommand extends Command
|
|||
return [['No tags found', '-', '-']];
|
||||
}
|
||||
|
||||
return map(
|
||||
$tags,
|
||||
return array_map(
|
||||
static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsSummary->total],
|
||||
[...$tags],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_keys;
|
||||
use function Functional\map;
|
||||
use function Functional\select_keys;
|
||||
use function array_map;
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\select_keys;
|
||||
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
|
||||
|
||||
abstract class AbstractVisitsListCommand extends Command
|
||||
|
@ -49,7 +49,7 @@ abstract class AbstractVisitsListCommand extends Command
|
|||
private function resolveRowsAndHeaders(Paginator $paginator): array
|
||||
{
|
||||
$extraKeys = [];
|
||||
$rows = map($paginator->getCurrentPageResults(), function (Visit $visit) use (&$extraKeys) {
|
||||
$rows = array_map(function (Visit $visit) use (&$extraKeys) {
|
||||
$extraFields = $this->mapExtraFields($visit);
|
||||
$extraKeys = array_keys($extraFields);
|
||||
|
||||
|
@ -60,9 +60,10 @@ abstract class AbstractVisitsListCommand extends Command
|
|||
...$extraFields,
|
||||
];
|
||||
|
||||
// Filter out unknown keys
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]);
|
||||
});
|
||||
$extra = map($extraKeys, camelCaseToHumanFriendly(...));
|
||||
}, [...$paginator->getCurrentPageResults()]);
|
||||
$extra = array_map(camelCaseToHumanFriendly(...), $extraKeys);
|
||||
|
||||
return [
|
||||
$rows,
|
||||
|
|
|
@ -8,30 +8,30 @@ use Symfony\Component\Console\Helper\Table;
|
|||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\intersperse;
|
||||
use function array_pop;
|
||||
|
||||
final class ShlinkTable
|
||||
{
|
||||
private const DEFAULT_STYLE_NAME = 'default';
|
||||
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
|
||||
|
||||
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators)
|
||||
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators = false)
|
||||
{
|
||||
}
|
||||
|
||||
public static function default(OutputInterface $output): self
|
||||
{
|
||||
return new self(new Table($output), false);
|
||||
return new self(new Table($output));
|
||||
}
|
||||
|
||||
public static function withRowSeparators(OutputInterface $output): self
|
||||
{
|
||||
return new self(new Table($output), true);
|
||||
return new self(new Table($output), withRowSeparators: true);
|
||||
}
|
||||
|
||||
public static function fromBaseTable(Table $baseTable): self
|
||||
{
|
||||
return new self($baseTable, false);
|
||||
return new self($baseTable);
|
||||
}
|
||||
|
||||
public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
|
||||
|
@ -39,7 +39,7 @@ final class ShlinkTable
|
|||
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
|
||||
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
|
||||
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
|
||||
$tableRows = $this->withRowSeparators ? intersperse($rows, new TableSeparator()) : $rows;
|
||||
$tableRows = $this->withRowSeparators ? $this->addRowSeparators($rows) : $rows;
|
||||
|
||||
$table = clone $this->baseTable;
|
||||
$table->setStyle($style)
|
||||
|
@ -49,4 +49,20 @@ final class ShlinkTable
|
|||
->setHeaderTitle($headerTitle)
|
||||
->render();
|
||||
}
|
||||
|
||||
private function addRowSeparators(array $rows): array
|
||||
{
|
||||
$aggregation = [];
|
||||
$separator = new TableSeparator();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$aggregation[] = $row;
|
||||
$aggregation[] = $separator;
|
||||
}
|
||||
|
||||
// Remove last separator
|
||||
array_pop($aggregation);
|
||||
|
||||
return $aggregation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
|||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
|
||||
class RoleResolverTest extends TestCase
|
||||
{
|
||||
private RoleResolver $resolver;
|
||||
|
@ -49,10 +47,13 @@ class RoleResolverTest extends TestCase
|
|||
{
|
||||
$domain = self::domainWithId(Domain::withAuthority('example.com'));
|
||||
$buildInput = static fn (array $definition) => function (TestCase $test) use ($definition): InputInterface {
|
||||
$returnMap = [];
|
||||
foreach ($definition as $param => $returnValue) {
|
||||
$returnMap[] = [$param, $returnValue];
|
||||
}
|
||||
|
||||
$input = $test->createStub(InputInterface::class);
|
||||
$input->method('getOption')->willReturnMap(
|
||||
map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
|
||||
);
|
||||
$input->method('getOption')->willReturnMap($returnMap);
|
||||
|
||||
return $input;
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
|||
use Symfony\Component\Lock;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
|
||||
class GeolocationDbUpdaterTest extends TestCase
|
||||
|
@ -128,7 +128,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
return [$days % 2 === 0 ? $timestamp : (string) $timestamp];
|
||||
};
|
||||
|
||||
return map(range(0, 34), $generateParamsWithTimestamp);
|
||||
return array_map($generateParamsWithTimestamp, range(0, 34));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
74
module/Core/functions/array-utils.php
Normal file
74
module/Core/functions/array-utils.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ArrayUtils;
|
||||
|
||||
use function array_filter;
|
||||
use function array_reduce;
|
||||
use function in_array;
|
||||
|
||||
use const ARRAY_FILTER_USE_KEY;
|
||||
|
||||
function contains(mixed $value, array $array): bool
|
||||
{
|
||||
return in_array($value, $array, strict: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $multiArray
|
||||
* @return array
|
||||
*/
|
||||
function flatten(array $multiArray): array
|
||||
{
|
||||
return array_reduce(
|
||||
$multiArray,
|
||||
static fn (array $carry, array $value) => [...$carry, ...$value],
|
||||
initial: [],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a callback returns true for at least one item in a collection.
|
||||
* @param callable(mixed $value, mixed $key): bool $callback
|
||||
*/
|
||||
function some(iterable $collection, callable $callback): bool
|
||||
{
|
||||
foreach ($collection as $key => $value) {
|
||||
if ($callback($value, $key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a callback returns true for all item in a collection.
|
||||
* @param callable(mixed $value, string|number $key): bool $callback
|
||||
*/
|
||||
function every(iterable $collection, callable $callback): bool
|
||||
{
|
||||
foreach ($collection as $key => $value) {
|
||||
if (! $callback($value, $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing only those entries in the array whose key is in the supplied keys.
|
||||
*/
|
||||
function select_keys(array $array, array $keys): array
|
||||
{
|
||||
return array_filter(
|
||||
$array,
|
||||
static fn (string $key) => contains(
|
||||
$key,
|
||||
$keys,
|
||||
),
|
||||
ARRAY_FILTER_USE_KEY,
|
||||
);
|
||||
}
|
|
@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Core;
|
|||
|
||||
use BackedEnum;
|
||||
use Cake\Chronos\Chronos;
|
||||
use Cake\Chronos\ChronosInterface;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
||||
use Jaybizzle\CrawlerDetect\CrawlerDetect;
|
||||
|
@ -17,9 +16,10 @@ use PUGX\Shortid\Factory as ShortIdFactory;
|
|||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_reduce;
|
||||
use function date_default_timezone_get;
|
||||
use function Functional\map;
|
||||
use function Functional\reduce_left;
|
||||
use function is_array;
|
||||
use function print_r;
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
|
@ -57,7 +57,7 @@ function parseDateRangeFromQuery(array $query, string $startDateName, string $en
|
|||
/**
|
||||
* @return ($date is null ? null : Chronos)
|
||||
*/
|
||||
function normalizeOptionalDate(string|DateTimeInterface|ChronosInterface|null $date): ?Chronos
|
||||
function normalizeOptionalDate(string|DateTimeInterface|Chronos|null $date): ?Chronos
|
||||
{
|
||||
$parsedDate = match (true) {
|
||||
$date === null || $date instanceof Chronos => $date,
|
||||
|
@ -68,7 +68,7 @@ function normalizeOptionalDate(string|DateTimeInterface|ChronosInterface|null $d
|
|||
return $parsedDate?->setTimezone(date_default_timezone_get());
|
||||
}
|
||||
|
||||
function normalizeDate(string|DateTimeInterface|ChronosInterface $date): Chronos
|
||||
function normalizeDate(string|DateTimeInterface|Chronos $date): Chronos
|
||||
{
|
||||
return normalizeOptionalDate($date);
|
||||
}
|
||||
|
@ -94,10 +94,12 @@ function getNonEmptyOptionalValueFromInputFilter(InputFilter $inputFilter, strin
|
|||
function arrayToString(array $array, int $indentSize = 4): string
|
||||
{
|
||||
$indent = str_repeat(' ', $indentSize);
|
||||
$names = array_keys($array);
|
||||
$index = 0;
|
||||
|
||||
return reduce_left($array, static function ($messages, string $name, $_, string $acc) use (&$index, $indent) {
|
||||
return array_reduce($names, static function (string $acc, string $name) use (&$index, $indent, $array) {
|
||||
$index++;
|
||||
$messages = $array[$name];
|
||||
|
||||
return $acc . sprintf(
|
||||
"%s%s'%s' => %s",
|
||||
|
@ -177,6 +179,6 @@ function enumValues(string $enum): array
|
|||
}
|
||||
|
||||
return $cache[$enum] ?? (
|
||||
$cache[$enum] = map($enum::cases(), static fn (BackedEnum $type) => (string) $type->value)
|
||||
$cache[$enum] = array_map(static fn (BackedEnum $type) => (string) $type->value, $enum::cases())
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ use Endroid\QrCode\Writer\WriterInterface;
|
|||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Core\Options\QrCodeOptions;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
|
@ -74,7 +74,7 @@ final class QrCodeParams
|
|||
private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface
|
||||
{
|
||||
$qFormat = self::normalizeParam($query['format'] ?? '');
|
||||
$format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format);
|
||||
$format = contains($qFormat, self::SUPPORTED_FORMATS) ? $qFormat : self::normalizeParam($defaults->format);
|
||||
|
||||
return match ($format) {
|
||||
'svg' => new SvgWriter(),
|
||||
|
|
|
@ -12,8 +12,6 @@ use Psr\Log\LoggerInterface;
|
|||
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;
|
||||
|
||||
|
@ -23,8 +21,8 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
|
|||
private const ORIGINAL_PATH_PLACEHOLDER = '{ORIGINAL_PATH}';
|
||||
|
||||
public function __construct(
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper,
|
||||
private LoggerInterface $logger,
|
||||
private readonly RedirectResponseHelperInterface $redirectResponseHelper,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -52,9 +50,6 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
|
|||
|
||||
private function resolvePlaceholders(UriInterface $currentUri, string $redirectUrl): string
|
||||
{
|
||||
$domain = $currentUri->getAuthority();
|
||||
$path = $currentUri->getPath();
|
||||
|
||||
try {
|
||||
$redirectUri = Uri::createFromString($redirectUrl);
|
||||
} catch (SyntaxError $e) {
|
||||
|
@ -65,18 +60,32 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
|
|||
return $redirectUrl;
|
||||
}
|
||||
|
||||
$replacePlaceholderForPattern = static fn (string $pattern, string $replace, callable $modifier) =>
|
||||
static fn (?string $value) =>
|
||||
$value === null ? null : str_replace($modifier($pattern), $modifier($replace), $value);
|
||||
$replacePlaceholders = static fn (callable $modifier) => compose(
|
||||
$replacePlaceholderForPattern(self::DOMAIN_PLACEHOLDER, $domain, $modifier),
|
||||
$replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier),
|
||||
$path = $currentUri->getPath();
|
||||
$domain = $currentUri->getAuthority();
|
||||
|
||||
$replacePlaceholderForPattern = static fn (string $pattern, string $replace, ?string $value): string|null =>
|
||||
$value === null ? null : str_replace($pattern, $replace, $value);
|
||||
|
||||
$replacePlaceholders = static function (
|
||||
callable $modifier,
|
||||
?string $value,
|
||||
) use (
|
||||
$replacePlaceholderForPattern,
|
||||
$path,
|
||||
$domain,
|
||||
): string|null {
|
||||
$value = $replacePlaceholderForPattern($modifier(self::DOMAIN_PLACEHOLDER), $modifier($domain), $value);
|
||||
return $replacePlaceholderForPattern($modifier(self::ORIGINAL_PATH_PLACEHOLDER), $modifier($path), $value);
|
||||
};
|
||||
|
||||
$replacePlaceholdersInPath = static function (string $path) use ($replacePlaceholders): string {
|
||||
$result = $replacePlaceholders(static fn (mixed $v) => $v, $path);
|
||||
return str_replace('//', '/', $result ?? '');
|
||||
};
|
||||
$replacePlaceholdersInQuery = static fn (?string $query): string|null => $replacePlaceholders(
|
||||
urlencode(...),
|
||||
$query,
|
||||
);
|
||||
$replacePlaceholdersInPath = compose(
|
||||
$replacePlaceholders(id(...)),
|
||||
static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path),
|
||||
);
|
||||
$replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...));
|
||||
|
||||
return $redirectUri
|
||||
->withPath($replacePlaceholdersInPath($redirectUri->getPath()))
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Config\PostProcessor;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class BasePathPrefixer
|
||||
{
|
||||
|
@ -23,13 +23,13 @@ class BasePathPrefixer
|
|||
|
||||
private function prefixPathsWithBasePath(string $configKey, array $config, string $basePath): array
|
||||
{
|
||||
return map($config[$configKey] ?? [], function (array $element) use ($basePath) {
|
||||
return array_map(function (array $element) use ($basePath) {
|
||||
if (! isset($element['path'])) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$element['path'] = $basePath . $element['path'];
|
||||
return $element;
|
||||
});
|
||||
}, $config[$configKey] ?? []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Config\PostProcessor;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function str_replace;
|
||||
|
||||
class MultiSegmentSlugProcessor
|
||||
|
@ -19,11 +19,11 @@ class MultiSegmentSlugProcessor
|
|||
return $config;
|
||||
}
|
||||
|
||||
$config['routes'] = map($config['routes'] ?? [], static function (array $route): array {
|
||||
$config['routes'] = array_map(static function (array $route): array {
|
||||
['path' => $path] = $route;
|
||||
$route['path'] = str_replace(self::SINGLE_SEGMENT_PATTERN, self::MULTI_SEGMENT_PATTERN, $path);
|
||||
return $route;
|
||||
});
|
||||
}, $config['routes'] ?? []);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
|
|
@ -9,25 +9,34 @@ use Mezzio\Router\Route;
|
|||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectStatus;
|
||||
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function Functional\partition;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
|
||||
|
||||
/**
|
||||
* Sets the appropriate allowed methods on the redirect route, based on the redirect status code that was configured.
|
||||
* * For "legacy" status codes (301 and 302) the redirect URL will work only on GET method.
|
||||
* * For other status codes (307 and 308) the redirect URL will work on any method.
|
||||
*/
|
||||
class ShortUrlMethodsProcessor
|
||||
{
|
||||
public function __invoke(array $config): array
|
||||
{
|
||||
[$redirectRoutes, $rest] = partition(
|
||||
$config['routes'] ?? [],
|
||||
static fn (array $route) => $route['name'] === RedirectAction::class,
|
||||
);
|
||||
if (count($redirectRoutes) === 0) {
|
||||
$allRoutes = $config['routes'] ?? [];
|
||||
$redirectRoute = null;
|
||||
$rest = [];
|
||||
|
||||
// Get default route from routes array
|
||||
foreach ($allRoutes as $route) {
|
||||
if ($route['name'] === RedirectAction::class) {
|
||||
$redirectRoute ??= $route;
|
||||
} else {
|
||||
$rest[] = $route;
|
||||
}
|
||||
}
|
||||
|
||||
if ($redirectRoute === null) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
[$redirectRoute] = array_values($redirectRoutes);
|
||||
$redirectStatus = RedirectStatus::tryFrom(
|
||||
$config['redirects']['redirect_status_code'] ?? 0,
|
||||
) ?? DEFAULT_REDIRECT_STATUS_CODE;
|
||||
|
|
|
@ -14,9 +14,7 @@ use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
|||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function Functional\first;
|
||||
use function Functional\group;
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class DomainService implements DomainServiceInterface
|
||||
{
|
||||
|
@ -30,7 +28,7 @@ class DomainService implements DomainServiceInterface
|
|||
public function listDomains(?ApiKey $apiKey = null): array
|
||||
{
|
||||
[$default, $domains] = $this->defaultDomainAndRest($apiKey);
|
||||
$mappedDomains = map($domains, fn (Domain $domain) => DomainItem::forNonDefaultDomain($domain));
|
||||
$mappedDomains = array_map(fn (Domain $domain) => DomainItem::forNonDefaultDomain($domain), $domains);
|
||||
|
||||
if ($apiKey?->hasRole(Role::DOMAIN_SPECIFIC)) {
|
||||
return $mappedDomains;
|
||||
|
@ -49,12 +47,19 @@ class DomainService implements DomainServiceInterface
|
|||
{
|
||||
/** @var DomainRepositoryInterface $repo */
|
||||
$repo = $this->em->getRepository(Domain::class);
|
||||
$groups = group(
|
||||
$repo->findDomains($apiKey),
|
||||
fn (Domain $domain) => $domain->authority === $this->defaultDomain ? 'default' : 'domains',
|
||||
);
|
||||
$allDomains = $repo->findDomains($apiKey);
|
||||
$defaultDomain = null;
|
||||
$restOfDomains = [];
|
||||
|
||||
return [first($groups['default'] ?? []), $groups['domains'] ?? []];
|
||||
foreach ($allDomains as $domain) {
|
||||
if ($domain->authority === $this->defaultDomain) {
|
||||
$defaultDomain = $domain;
|
||||
} else {
|
||||
$restOfDomains[] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
return [$defaultDomain, $restOfDomains];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,7 @@ use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
|
|||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\each;
|
||||
use function array_walk;
|
||||
|
||||
abstract class AbstractNotifyVisitListener extends AbstractAsyncListener
|
||||
{
|
||||
|
@ -46,7 +46,7 @@ abstract class AbstractNotifyVisitListener extends AbstractAsyncListener
|
|||
$updates = $this->determineUpdatesForVisit($visit);
|
||||
|
||||
try {
|
||||
each($updates, fn (Update $update) => $this->publishingHelper->publishUpdate($update));
|
||||
array_walk($updates, fn (Update $update) => $this->publishingHelper->publishUpdate($update));
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->debug(
|
||||
'Error while trying to notify {name} with new visit. {e}',
|
||||
|
|
|
@ -19,18 +19,18 @@ use Shlinkio\Shlink\Core\Options\WebhookOptions;
|
|||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
/** @deprecated */
|
||||
class NotifyVisitToWebHooks
|
||||
{
|
||||
public function __construct(
|
||||
private ClientInterface $httpClient,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger,
|
||||
private WebhookOptions $webhookOptions,
|
||||
private DataTransformerInterface $transformer,
|
||||
private AppOptions $appOptions,
|
||||
private readonly ClientInterface $httpClient,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly WebhookOptions $webhookOptions,
|
||||
private readonly DataTransformerInterface $transformer,
|
||||
private readonly AppOptions $appOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -82,11 +82,11 @@ class NotifyVisitToWebHooks
|
|||
*/
|
||||
private function performRequests(array $requestOptions, string $visitId): array
|
||||
{
|
||||
return map(
|
||||
$this->webhookOptions->webhooks(),
|
||||
return array_map(
|
||||
fn (string $webhook): PromiseInterface => $this->httpClient
|
||||
->requestAsync(RequestMethodInterface::METHOD_POST, $webhook, $requestOptions)
|
||||
->otherwise(fn (Throwable $e) => $this->logWebhookFailure($webhook, $visitId, $e)),
|
||||
$this->webhookOptions->webhooks(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,9 @@ use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit;
|
|||
use function Shlinkio\Shlink\Core\normalizeDate;
|
||||
use function sprintf;
|
||||
|
||||
final class ShortUrlImporting
|
||||
final readonly class ShortUrlImporting
|
||||
{
|
||||
private function __construct(private readonly ShortUrl $shortUrl, private readonly bool $isNew)
|
||||
private function __construct(private ShortUrl $shortUrl, private bool $isNew)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -57,11 +57,18 @@ final class ShortUrlImporting
|
|||
|
||||
private function resolveShortUrl(EntityManagerInterface $em): ShortUrl
|
||||
{
|
||||
// If wrapped ShortUrl has no ID, avoid trying to query the EM, as it would fail in Postgres.
|
||||
// See https://github.com/shlinkio/shlink/issues/1947
|
||||
$id = $this->shortUrl->getId();
|
||||
if (!$id) {
|
||||
return $this->shortUrl;
|
||||
}
|
||||
|
||||
// Instead of directly accessing wrapped ShortUrl entity, try to get it from the EM.
|
||||
// With this, we will get the same entity from memory if it is known by the EM, but if it was cleared, the EM
|
||||
// will fetch it again from the database, preventing errors at runtime.
|
||||
// However, if the EM was not flushed yet, the entity will not be found by ID, but it is known by the EM.
|
||||
// In that case, we fall back to wrapped ShortUrl entity directly.
|
||||
return $em->find(ShortUrl::class, $this->shortUrl->getId()) ?? $this->shortUrl;
|
||||
return $em->find(ShortUrl::class, $id) ?? $this->shortUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
|||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function Functional\map;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
||||
use function Shlinkio\Shlink\Core\normalizeDate;
|
||||
|
@ -90,9 +90,9 @@ class ShortUrl extends AbstractEntity
|
|||
$instance->longUrl = $creation->getLongUrl();
|
||||
$instance->dateCreated = Chronos::now();
|
||||
$instance->visits = new ArrayCollection();
|
||||
$instance->deviceLongUrls = new ArrayCollection(map(
|
||||
$creation->deviceLongUrls,
|
||||
$instance->deviceLongUrls = new ArrayCollection(array_map(
|
||||
fn (DeviceLongUrlPair $pair) => DeviceLongUrl::fromShortUrlAndPair($instance, $pair),
|
||||
$creation->deviceLongUrls,
|
||||
));
|
||||
$instance->tags = $relationResolver->resolveTags($creation->tags);
|
||||
$instance->validSince = $creation->validSince;
|
||||
|
|
|
@ -6,9 +6,6 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
|||
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
|
||||
use function array_values;
|
||||
use function Functional\group;
|
||||
use function Functional\map;
|
||||
use function trim;
|
||||
|
||||
final class DeviceLongUrlPair
|
||||
|
@ -27,20 +24,21 @@ final class DeviceLongUrlPair
|
|||
* * The first one is a list of mapped instances for those entries in the map with non-null value
|
||||
* * The second is a list of DeviceTypes which have been provided with value null
|
||||
*
|
||||
* @param array<string, string> $map
|
||||
* @param array<string, string|null> $map
|
||||
* @return array{array<string, self>, DeviceType[]}
|
||||
*/
|
||||
public static function fromMapToChangeSet(array $map): array
|
||||
{
|
||||
$typesWithNullUrl = group($map, static fn (?string $longUrl) => $longUrl === null ? 'remove' : 'keep');
|
||||
$deviceTypesToRemove = array_values(map(
|
||||
$typesWithNullUrl['remove'] ?? [],
|
||||
static fn ($_, string $deviceType) => DeviceType::from($deviceType),
|
||||
));
|
||||
$pairsToKeep = map(
|
||||
$typesWithNullUrl['keep'] ?? [],
|
||||
fn (string $longUrl, string $deviceType) => self::fromRawTypeAndLongUrl($deviceType, $longUrl),
|
||||
);
|
||||
$pairsToKeep = [];
|
||||
$deviceTypesToRemove = [];
|
||||
|
||||
foreach ($map as $deviceType => $longUrl) {
|
||||
if ($longUrl === null) {
|
||||
$deviceTypesToRemove[] = DeviceType::from($deviceType);
|
||||
} else {
|
||||
$pairsToKeep[$deviceType] = self::fromRawTypeAndLongUrl($deviceType, $longUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return [$pairsToKeep, $deviceTypesToRemove];
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
enum OrderableField: string
|
||||
{
|
||||
|
@ -16,8 +16,8 @@ enum OrderableField: string
|
|||
public static function isBasicField(string $value): bool
|
||||
{
|
||||
return contains(
|
||||
[self::LONG_URL->value, self::SHORT_CODE->value, self::DATE_CREATED->value, self::TITLE->value],
|
||||
$value,
|
||||
[self::LONG_URL->value, self::SHORT_CODE->value, self::DATE_CREATED->value, self::TITLE->value],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
|
|||
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function Functional\contains;
|
||||
use function Functional\every;
|
||||
use function is_array;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\every;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
class DeviceLongUrlsValidator extends AbstractValidator
|
||||
|
@ -41,7 +41,7 @@ class DeviceLongUrlsValidator extends AbstractValidator
|
|||
|
||||
$validValues = enumValues(DeviceType::class);
|
||||
$keys = array_keys($value);
|
||||
if (! every($keys, static fn ($key) => contains($validValues, $key))) {
|
||||
if (! every($keys, static fn ($key) => contains($key, $validValues))) {
|
||||
$this->error(self::INVALID_DEVICE);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -15,9 +15,8 @@ use Symfony\Component\Lock\Lock;
|
|||
use Symfony\Component\Lock\LockFactory;
|
||||
use Symfony\Component\Lock\Store\InMemoryStore;
|
||||
|
||||
use function Functional\invoke;
|
||||
use function Functional\map;
|
||||
use function Functional\unique;
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
|
||||
class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
||||
{
|
||||
|
@ -74,10 +73,10 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
|
|||
return new Collections\ArrayCollection();
|
||||
}
|
||||
|
||||
$tags = unique($tags);
|
||||
$tags = array_unique($tags);
|
||||
$repo = $this->em->getRepository(Tag::class);
|
||||
|
||||
return new Collections\ArrayCollection(map($tags, function (string $tagName) use ($repo): Tag {
|
||||
return new Collections\ArrayCollection(array_map(function (string $tagName) use ($repo): Tag {
|
||||
$this->lock($this->tagLocks, 'tag_' . $tagName);
|
||||
|
||||
$existingTag = $repo->findOneBy(['name' => $tagName]);
|
||||
|
@ -91,7 +90,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
|
|||
$this->em->persist($tag);
|
||||
|
||||
return $tag;
|
||||
}));
|
||||
}, $tags));
|
||||
}
|
||||
|
||||
private function memoizeNewTag(string $tagName): Tag
|
||||
|
@ -110,6 +109,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
|
|||
$lock->acquire(true);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @param array<string, Lock> $locks
|
||||
*/
|
||||
|
@ -126,9 +126,15 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
|
|||
$this->memoizedNewTags = [];
|
||||
|
||||
// Release all locks
|
||||
invoke($this->tagLocks, 'release');
|
||||
invoke($this->domainLocks, 'release');
|
||||
$this->tagLocks = [];
|
||||
$this->domainLocks = [];
|
||||
$this->releaseLocks($this->tagLocks);
|
||||
$this->releaseLocks($this->domainLocks);
|
||||
}
|
||||
|
||||
private function releaseLocks(array &$locks): void
|
||||
{
|
||||
foreach ($locks as $tagLock) {
|
||||
$tagLock->release();
|
||||
}
|
||||
$locks = [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use Doctrine\Common\Collections;
|
|||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
||||
{
|
||||
|
@ -23,6 +23,6 @@ class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterfac
|
|||
*/
|
||||
public function resolveTags(array $tags): Collections\Collection
|
||||
{
|
||||
return new Collections\ArrayCollection(map($tags, fn (string $tag) => new Tag($tag)));
|
||||
return new Collections\ArrayCollection(array_map(fn (string $tag) => new Tag($tag), $tags));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Transformer;
|
|||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
|
||||
|
||||
use function Functional\invoke;
|
||||
use function Functional\invoke_if;
|
||||
use function array_map;
|
||||
|
||||
class ShortUrlDataTransformer implements DataTransformerInterface
|
||||
{
|
||||
|
@ -29,7 +29,7 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
|||
'longUrl' => $shortUrl->getLongUrl(),
|
||||
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'tags' => invoke($shortUrl->getTags(), '__toString'),
|
||||
'tags' => array_map(static fn (Tag $tag) => $tag->__toString(), $shortUrl->getTags()->toArray()),
|
||||
'meta' => $this->buildMeta($shortUrl),
|
||||
'domain' => $shortUrl->getDomain(),
|
||||
'title' => $shortUrl->title(),
|
||||
|
@ -52,8 +52,8 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
|||
$maxVisits = $shortUrl->getMaxVisits();
|
||||
|
||||
return [
|
||||
'validSince' => invoke_if($validSince, 'toAtomString'),
|
||||
'validUntil' => invoke_if($validUntil, 'toAtomString'),
|
||||
'validSince' => $validSince?->toAtomString(),
|
||||
'validUntil' => $validUntil?->toAtomString(),
|
||||
'maxVisits' => $maxVisits,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ use Shlinkio\Shlink\Rest\ApiKey\Role;
|
|||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function Functional\each;
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function array_walk;
|
||||
use function Shlinkio\Shlink\Core\camelCaseToSnakeCase;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
|
@ -95,7 +95,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
|||
$nonBotVisitsSubQb = $buildVisitsSubQb(true, 'non_bot_visits');
|
||||
|
||||
// Apply API key specification to all sub-queries
|
||||
each([$tagsSubQb, $allVisitsSubQb, $nonBotVisitsSubQb], $applyApiKeyToNativeQb);
|
||||
$queryBuilders = [$tagsSubQb, $allVisitsSubQb, $nonBotVisitsSubQb];
|
||||
array_walk($queryBuilders, $applyApiKeyToNativeQb);
|
||||
|
||||
// A native query builder needs to be used here, because DQL and ORM query builders do not support
|
||||
// sub-queries at "from" and "join" level.
|
||||
|
@ -126,9 +127,9 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
|||
$rsm->addScalarResult('non_bot_visits', 'nonBotVisits');
|
||||
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
|
||||
|
||||
return map(
|
||||
$this->getEntityManager()->createNativeQuery($mainQb->getSQL(), $rsm)->getResult(),
|
||||
return array_map(
|
||||
TagInfo::fromRawData(...),
|
||||
$this->getEntityManager()->createNativeQuery($mainQb->getSQL(), $rsm)->getResult(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Util;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
enum RedirectStatus: int
|
||||
{
|
||||
|
@ -13,11 +13,11 @@ enum RedirectStatus: int
|
|||
|
||||
public function allowsCache(): bool
|
||||
{
|
||||
return contains([self::STATUS_301, self::STATUS_308], $this);
|
||||
return contains($this, [self::STATUS_301, self::STATUS_308]);
|
||||
}
|
||||
|
||||
public function isLegacyStatus(): bool
|
||||
{
|
||||
return contains([self::STATUS_301, self::STATUS_302], $this);
|
||||
return contains($this, [self::STATUS_301, self::STATUS_302]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,11 @@ use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function explode;
|
||||
use function Functional\map;
|
||||
use function Functional\some;
|
||||
use function implode;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
use function str_contains;
|
||||
|
||||
class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
|
||||
|
@ -96,11 +97,15 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
|
|||
|
||||
private function parseValueWithWildcards(string $value, array $remoteAddrParts): ?RangeInterface
|
||||
{
|
||||
$octets = explode('.', $value);
|
||||
$keys = array_keys($octets);
|
||||
|
||||
// Replace wildcard parts with the corresponding ones from the remote address
|
||||
return Factory::parseRangeString(
|
||||
implode('.', map(
|
||||
explode('.', $value),
|
||||
implode('.', array_map(
|
||||
fn (string $part, int $index) => $part === '*' ? $remoteAddrParts[$index] : $part,
|
||||
$octets,
|
||||
$keys,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
class ShortUrlListRepositoryTest extends DatabaseTestCase
|
||||
|
@ -60,22 +60,22 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||
$this->getEntityManager()->persist($foo);
|
||||
|
||||
$bar = ShortUrl::withLongUrl('https://bar');
|
||||
$visits = map(range(0, 5), function () use ($bar) {
|
||||
$visits = array_map(function () use ($bar) {
|
||||
$visit = Visit::forValidShortUrl($bar, Visitor::botInstance());
|
||||
$this->getEntityManager()->persist($visit);
|
||||
|
||||
return $visit;
|
||||
});
|
||||
}, range(0, 5));
|
||||
$bar->setVisits(new ArrayCollection($visits));
|
||||
$this->getEntityManager()->persist($bar);
|
||||
|
||||
$foo2 = ShortUrl::withLongUrl('https://foo_2');
|
||||
$visits2 = map(range(0, 3), function () use ($foo2) {
|
||||
$visits2 = array_map(function () use ($foo2) {
|
||||
$visit = Visit::forValidShortUrl($foo2, Visitor::emptyInstance());
|
||||
$this->getEntityManager()->persist($visit);
|
||||
|
||||
return $visit;
|
||||
});
|
||||
}, range(0, 3));
|
||||
$foo2->setVisits(new ArrayCollection($visits2));
|
||||
$ref = new ReflectionObject($foo2);
|
||||
$dateProp = $ref->getProperty('dateCreated');
|
||||
|
|
|
@ -12,7 +12,7 @@ use Shlinkio\Shlink\Core\Tag\Paginator\Adapter\TagsPaginatorAdapter;
|
|||
use Shlinkio\Shlink\Core\Tag\Repository\TagRepository;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class TagsPaginatorAdapterTest extends DatabaseTestCase
|
||||
{
|
||||
|
@ -47,7 +47,7 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
|
|||
'orderBy' => $orderBy,
|
||||
]), null);
|
||||
|
||||
$tagNames = map($adapter->getSlice($offset, $length), static fn (Tag $tag) => $tag->__toString());
|
||||
$tagNames = array_map(static fn (Tag $tag) => $tag->__toString(), [...$adapter->getSlice($offset, $length)]);
|
||||
|
||||
self::assertEquals($expectedTags, $tagNames);
|
||||
self::assertEquals($expectedTotalCount, $adapter->getNbResults());
|
||||
|
|
|
@ -14,7 +14,7 @@ use Shlinkio\Shlink\Core\Visit\Repository\VisitLocationRepository;
|
|||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
|
||||
class VisitLocationRepositoryTest extends DatabaseTestCase
|
||||
|
@ -57,6 +57,6 @@ class VisitLocationRepositoryTest extends DatabaseTestCase
|
|||
|
||||
public static function provideBlockSize(): iterable
|
||||
{
|
||||
return map(range(1, 10), fn (int $value) => [$value]);
|
||||
return array_map(static fn (int $value) => [$value], range(1, 10));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
|
||||
use function count;
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
class NotifyVisitToWebHooksTest extends TestCase
|
||||
{
|
||||
|
@ -102,7 +102,7 @@ class NotifyVisitToWebHooksTest extends TestCase
|
|||
return true;
|
||||
}),
|
||||
)->willReturnCallback(function ($_, $webhook) use ($invalidWebhooks) {
|
||||
$shouldReject = contains($invalidWebhooks, $webhook);
|
||||
$shouldReject = contains($webhook, $invalidWebhooks);
|
||||
return $shouldReject ? new RejectedPromise(new Exception('')) : new FulfilledPromise('');
|
||||
});
|
||||
$this->logger->expects($this->exactly(count($invalidWebhooks)))->method('warning')->with(
|
||||
|
|
|
@ -27,9 +27,8 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
|||
use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer;
|
||||
use Throwable;
|
||||
|
||||
use function array_walk;
|
||||
use function count;
|
||||
use function Functional\each;
|
||||
use function Functional\noop;
|
||||
|
||||
class NotifyVisitToRabbitMqTest extends TestCase
|
||||
{
|
||||
|
@ -77,7 +76,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
{
|
||||
$visitId = '123';
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit);
|
||||
each($expectedChannels, function (string $method): void {
|
||||
array_walk($expectedChannels, function (string $method): void {
|
||||
$this->updatesGenerator->expects($this->once())->method($method)->with(
|
||||
$this->isInstanceOf(Visit::class),
|
||||
)->willReturn(Update::forTopicAndPayload('', []));
|
||||
|
@ -153,7 +152,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
yield 'legacy non-orphan visit' => [
|
||||
true,
|
||||
$visit = Visit::forValidShortUrl(ShortUrl::withLongUrl('https://longUrl'), Visitor::emptyInstance()),
|
||||
noop(...),
|
||||
static fn () => null,
|
||||
function (MockObject & PublishingHelperInterface $helper) use ($visit): void {
|
||||
$helper->method('publishUpdate')->with(self::callback(function (Update $update) use ($visit): bool {
|
||||
$payload = $update->payload;
|
||||
|
@ -170,7 +169,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
yield 'legacy orphan visit' => [
|
||||
true,
|
||||
Visit::forBasePath(Visitor::emptyInstance()),
|
||||
noop(...),
|
||||
static fn () => null,
|
||||
function (MockObject & PublishingHelperInterface $helper): void {
|
||||
$helper->method('publishUpdate')->with(self::callback(function (Update $update): bool {
|
||||
$payload = $update->payload;
|
||||
|
|
|
@ -16,7 +16,7 @@ use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
|||
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class UpdateGeoLiteDbTest extends TestCase
|
||||
{
|
||||
|
@ -124,9 +124,9 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||
|
||||
public static function provideGeolocationResults(): iterable
|
||||
{
|
||||
return map(GeolocationResult::cases(), static fn (GeolocationResult $value) => [
|
||||
return array_map(static fn (GeolocationResult $value) => [
|
||||
$value,
|
||||
$value === GeolocationResult::DB_CREATED ? 1 : 0,
|
||||
]);
|
||||
], GeolocationResult::cases());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase;
|
|||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
||||
use function sprintf;
|
||||
|
@ -42,13 +42,13 @@ class DeleteShortUrlExceptionTest extends TestCase
|
|||
|
||||
public static function provideThresholds(): array
|
||||
{
|
||||
return map(range(5, 50, 5), function (int $number) {
|
||||
return array_map(function (int $number) {
|
||||
return [$number, $shortCode = generateRandomShortCode(6), sprintf(
|
||||
'Impossible to delete short URL with short code "%s", since it has more than "%s" visits.',
|
||||
$shortCode,
|
||||
$number,
|
||||
)];
|
||||
});
|
||||
}, range(5, 50, 5));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -13,7 +13,7 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
class FunctionsTest extends TestCase
|
||||
|
@ -29,18 +29,21 @@ class FunctionsTest extends TestCase
|
|||
|
||||
public static function provideEnums(): iterable
|
||||
{
|
||||
yield EnvVars::class => [EnvVars::class, map(EnvVars::cases(), static fn (EnvVars $envVar) => $envVar->value)];
|
||||
yield EnvVars::class => [
|
||||
EnvVars::class,
|
||||
array_map(static fn (EnvVars $envVar) => $envVar->value, EnvVars::cases()),
|
||||
];
|
||||
yield VisitType::class => [
|
||||
VisitType::class,
|
||||
map(VisitType::cases(), static fn (VisitType $envVar) => $envVar->value),
|
||||
array_map(static fn (VisitType $envVar) => $envVar->value, VisitType::cases()),
|
||||
];
|
||||
yield DeviceType::class => [
|
||||
DeviceType::class,
|
||||
map(DeviceType::cases(), static fn (DeviceType $envVar) => $envVar->value),
|
||||
array_map(static fn (DeviceType $envVar) => $envVar->value, DeviceType::cases()),
|
||||
];
|
||||
yield OrderableField::class => [
|
||||
OrderableField::class,
|
||||
map(OrderableField::cases(), static fn (OrderableField $envVar) => $envVar->value),
|
||||
array_map(static fn (OrderableField $envVar) => $envVar->value, OrderableField::cases()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ use stdClass;
|
|||
use Symfony\Component\Console\Style\StyleInterface;
|
||||
|
||||
use function count;
|
||||
use function Functional\contains;
|
||||
use function Functional\some;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
|
@ -128,8 +128,8 @@ class ImportedLinksProcessorTest extends TestCase
|
|||
$this->em->method('getRepository')->with(ShortUrl::class)->willReturn($this->repo);
|
||||
$this->repo->expects($this->exactly(count($urls)))->method('findOneByImportedUrl')->willReturnCallback(
|
||||
fn (ImportedShlinkUrl $url): ?ShortUrl => contains(
|
||||
['https://foo', 'https://baz2', 'https://baz3'],
|
||||
$url->longUrl,
|
||||
['https://foo', 'https://baz2', 'https://baz3'],
|
||||
) ? ShortUrl::fromImport($url, true) : null,
|
||||
);
|
||||
$this->shortCodeHelper->expects($this->exactly(2))->method('ensureShortCodeUniqueness')->willReturn(true);
|
||||
|
@ -232,15 +232,20 @@ class ImportedLinksProcessorTest extends TestCase
|
|||
}
|
||||
|
||||
#[Test, DataProvider('provideFoundShortUrls')]
|
||||
public function visitsArePersistedWithProperShortUrl(?ShortUrl $foundShortUrl): void
|
||||
public function visitsArePersistedWithProperShortUrl(ShortUrl $originalShortUrl, ?ShortUrl $foundShortUrl): void
|
||||
{
|
||||
$originalShortUrl = ShortUrl::withLongUrl('https://foo');
|
||||
|
||||
$this->em->method('getRepository')->with(ShortUrl::class)->willReturn($this->repo);
|
||||
$this->repo->expects($this->once())->method('findOneByImportedUrl')->willReturn($originalShortUrl);
|
||||
$this->em->expects($this->exactly(2))->method('find')->willReturn($foundShortUrl);
|
||||
if (!$originalShortUrl->getId()) {
|
||||
$this->em->expects($this->never())->method('find');
|
||||
} else {
|
||||
$this->em->expects($this->exactly(2))->method('find')->willReturn($foundShortUrl);
|
||||
}
|
||||
$this->em->expects($this->once())->method('persist')->willReturnCallback(
|
||||
static fn (Visit $visit) => Assert::assertSame($foundShortUrl ?? $originalShortUrl, $visit->getShortUrl()),
|
||||
static fn (Visit $visit) => Assert::assertSame(
|
||||
$foundShortUrl ?? $originalShortUrl,
|
||||
$visit->getShortUrl(),
|
||||
),
|
||||
);
|
||||
|
||||
$now = Chronos::now();
|
||||
|
@ -253,8 +258,12 @@ class ImportedLinksProcessorTest extends TestCase
|
|||
|
||||
public static function provideFoundShortUrls(): iterable
|
||||
{
|
||||
yield [null];
|
||||
yield [ShortUrl::withLongUrl('https://bar')];
|
||||
yield 'not found new URL' => [ShortUrl::withLongUrl('https://foo')->setId('123'), null];
|
||||
yield 'found new URL' => [
|
||||
ShortUrl::withLongUrl('https://foo')->setId('123'),
|
||||
ShortUrl::withLongUrl('https://bar'),
|
||||
];
|
||||
yield 'old URL without ID' => [$originalShortUrl = ShortUrl::withLongUrl('https://foo'), $originalShortUrl];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,7 @@ use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
|||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -31,7 +31,7 @@ class DeleteShortUrlServiceTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$shortUrl = ShortUrl::createFake()->setVisits(new ArrayCollection(
|
||||
map(range(0, 10), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance())),
|
||||
array_map(fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()), range(0, 10)),
|
||||
));
|
||||
$this->shortCode = $shortUrl->getShortCode();
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
|||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
use Shlinkio\Shlink\Importer\Sources\ImportSource;
|
||||
|
||||
use function Functional\every;
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\every;
|
||||
use function strlen;
|
||||
use function strtolower;
|
||||
|
||||
|
@ -88,7 +88,7 @@ class ShortUrlTest extends TestCase
|
|||
public static function provideLengths(): iterable
|
||||
{
|
||||
yield [null, DEFAULT_SHORT_CODES_LENGTH];
|
||||
yield from map(range(4, 10), fn (int $value) => [$value, $value]);
|
||||
yield from array_map(fn (int $value) => [$value, $value], range(4, 10));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -16,9 +16,6 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
|
||||
|
||||
use function Functional\compose;
|
||||
use function Functional\const_function;
|
||||
|
||||
class TrimTrailingSlashMiddlewareTest extends TestCase
|
||||
{
|
||||
private MockObject & RequestHandlerInterface $requestHandler;
|
||||
|
@ -34,7 +31,10 @@ class TrimTrailingSlashMiddlewareTest extends TestCase
|
|||
ServerRequestInterface $inputRequest,
|
||||
callable $assertions,
|
||||
): void {
|
||||
$arg = compose($assertions, const_function(true));
|
||||
$arg = static function (...$args) use ($assertions): bool {
|
||||
$assertions(...$args);
|
||||
return true;
|
||||
};
|
||||
$this->requestHandler->expects($this->once())->method('handle')->with($this->callback($arg))->willReturn(
|
||||
new Response(),
|
||||
);
|
||||
|
|
|
@ -25,7 +25,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
|||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use ShlinkioTest\Shlink\Core\Util\ApiKeyDataProviders;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
|
||||
class ShortUrlResolverTest extends TestCase
|
||||
|
@ -113,9 +113,9 @@ class ShortUrlResolverTest extends TestCase
|
|||
$shortUrl = ShortUrl::create(
|
||||
ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://longUrl']),
|
||||
);
|
||||
$shortUrl->setVisits(new ArrayCollection(map(
|
||||
range(0, 4),
|
||||
$shortUrl->setVisits(new ArrayCollection(array_map(
|
||||
fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
|
||||
range(0, 4),
|
||||
)));
|
||||
|
||||
return $shortUrl;
|
||||
|
@ -132,9 +132,9 @@ class ShortUrlResolverTest extends TestCase
|
|||
'validUntil' => $now->subMonths(1)->toAtomString(),
|
||||
'longUrl' => 'https://longUrl',
|
||||
]));
|
||||
$shortUrl->setVisits(new ArrayCollection(map(
|
||||
range(0, 4),
|
||||
$shortUrl->setVisits(new ArrayCollection(array_map(
|
||||
fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
|
||||
range(0, 4),
|
||||
)));
|
||||
|
||||
return $shortUrl;
|
||||
|
|
|
@ -84,4 +84,14 @@ class ShortUrlDataTransformerTest extends TestCase
|
|||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function properTagsAreReturned(): void
|
||||
{
|
||||
['tags' => $tags] = $this->transformer->transform(ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
'longUrl' => 'https://longUrl',
|
||||
'tags' => ['foo', 'bar', 'baz'],
|
||||
])));
|
||||
self::assertEquals(['foo', 'bar', 'baz'], $tags);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
|||
use Shlinkio\Shlink\Core\Visit\Repository\VisitLocationRepositoryInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function floor;
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -45,12 +45,12 @@ class VisitLocatorTest extends TestCase
|
|||
string $serviceMethodName,
|
||||
string $expectedRepoMethodName,
|
||||
): void {
|
||||
$unlocatedVisits = map(
|
||||
range(1, 200),
|
||||
$unlocatedVisits = array_map(
|
||||
fn (int $i) => Visit::forValidShortUrl(
|
||||
ShortUrl::withLongUrl(sprintf('https://short_code_%s', $i)),
|
||||
Visitor::emptyInstance(),
|
||||
),
|
||||
range(1, 200),
|
||||
);
|
||||
|
||||
$this->repo->expects($this->once())->method($expectedRepoMethodName)->willReturn($unlocatedVisits);
|
||||
|
|
|
@ -33,8 +33,8 @@ use Shlinkio\Shlink\Core\Visit\VisitsStatsHelper;
|
|||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use ShlinkioTest\Shlink\Core\Util\ApiKeyDataProviders;
|
||||
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
class VisitsStatsHelperTest extends TestCase
|
||||
|
@ -75,8 +75,8 @@ class VisitsStatsHelperTest extends TestCase
|
|||
public static function provideCounts(): iterable
|
||||
{
|
||||
return [
|
||||
...map(range(0, 50, 5), fn (int $value) => [$value, null]),
|
||||
...map(range(0, 18, 3), fn (int $value) => [$value, ApiKey::create()]),
|
||||
...array_map(fn (int $value) => [$value, null], range(0, 50, 5)),
|
||||
...array_map(fn (int $value) => [$value, ApiKey::create()], range(0, 18, 3)),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,10 @@ class VisitsStatsHelperTest extends TestCase
|
|||
$repo = $this->createMock(ShortUrlRepositoryInterface::class);
|
||||
$repo->expects($this->once())->method('shortCodeIsInUse')->with($identifier, $spec)->willReturn(true);
|
||||
|
||||
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
|
||||
$list = array_map(
|
||||
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
|
||||
range(0, 1),
|
||||
);
|
||||
$repo2 = $this->createMock(VisitRepository::class);
|
||||
$repo2->method('findVisitsByShortCode')->with(
|
||||
$identifier,
|
||||
|
@ -147,7 +150,10 @@ class VisitsStatsHelperTest extends TestCase
|
|||
$repo = $this->createMock(TagRepository::class);
|
||||
$repo->expects($this->once())->method('tagExists')->with($tag, $apiKey)->willReturn(true);
|
||||
|
||||
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
|
||||
$list = array_map(
|
||||
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
|
||||
range(0, 1),
|
||||
);
|
||||
$repo2 = $this->createMock(VisitRepository::class);
|
||||
$repo2->method('findVisitsByTag')->with($tag, $this->isInstanceOf(VisitsListFiltering::class))->willReturn(
|
||||
$list,
|
||||
|
@ -185,7 +191,10 @@ class VisitsStatsHelperTest extends TestCase
|
|||
$repo = $this->createMock(DomainRepository::class);
|
||||
$repo->expects($this->once())->method('domainExists')->with($domain, $apiKey)->willReturn(true);
|
||||
|
||||
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
|
||||
$list = array_map(
|
||||
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
|
||||
range(0, 1),
|
||||
);
|
||||
$repo2 = $this->createMock(VisitRepository::class);
|
||||
$repo2->method('findVisitsByDomain')->with(
|
||||
$domain,
|
||||
|
@ -212,7 +221,10 @@ class VisitsStatsHelperTest extends TestCase
|
|||
$repo = $this->createMock(DomainRepository::class);
|
||||
$repo->expects($this->never())->method('domainExists');
|
||||
|
||||
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
|
||||
$list = array_map(
|
||||
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
|
||||
range(0, 1),
|
||||
);
|
||||
$repo2 = $this->createMock(VisitRepository::class);
|
||||
$repo2->method('findVisitsByDomain')->with(
|
||||
'DEFAULT',
|
||||
|
@ -236,7 +248,7 @@ class VisitsStatsHelperTest extends TestCase
|
|||
#[Test]
|
||||
public function orphanVisitsAreReturnedAsExpected(): void
|
||||
{
|
||||
$list = map(range(0, 3), fn () => Visit::forBasePath(Visitor::emptyInstance()));
|
||||
$list = array_map(static fn () => Visit::forBasePath(Visitor::emptyInstance()), range(0, 3));
|
||||
$repo = $this->createMock(VisitRepository::class);
|
||||
$repo->expects($this->once())->method('countOrphanVisits')->with(
|
||||
$this->isInstanceOf(VisitsCountFiltering::class),
|
||||
|
@ -254,7 +266,10 @@ class VisitsStatsHelperTest extends TestCase
|
|||
#[Test]
|
||||
public function nonOrphanVisitsAreReturnedAsExpected(): void
|
||||
{
|
||||
$list = map(range(0, 3), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
|
||||
$list = array_map(
|
||||
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
|
||||
range(0, 3),
|
||||
);
|
||||
$repo = $this->createMock(VisitRepository::class);
|
||||
$repo->expects($this->once())->method('countNonOrphanVisits')->with(
|
||||
$this->isInstanceOf(VisitsCountFiltering::class),
|
||||
|
|
|
@ -11,7 +11,7 @@ use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
|||
return [
|
||||
|
||||
'access_logs' => [
|
||||
'ignored_paths' => [
|
||||
'ignored_path_prefixes' => [
|
||||
Action\HealthAction::ROUTE_PATH,
|
||||
],
|
||||
],
|
||||
|
@ -20,7 +20,7 @@ return [
|
|||
ConfigAbstractFactory::class => [
|
||||
// Use MergeReplaceKey to overwrite what was defined in shlink-common, instead of merging it
|
||||
AccessLogMiddleware::class => new MergeReplaceKey(
|
||||
[AccessLogMiddleware::LOGGER_SERVICE_NAME, 'config.access_logs.ignored_paths'],
|
||||
[AccessLogMiddleware::LOGGER_SERVICE_NAME, 'config.access_logs.ignored_path_prefixes'],
|
||||
),
|
||||
],
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
|||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class ListTagsAction extends AbstractRestAction
|
||||
{
|
||||
|
@ -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, dataProp: 'stats');
|
||||
$rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag);
|
||||
$rawTags['data'] = array_map(static fn (TagInfo $info) => $info->tag, [...$tagsInfo]);
|
||||
|
||||
return new JsonResponse(['tags' => $rawTags]);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest;
|
||||
|
||||
use function Functional\first;
|
||||
use function Functional\map;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function reset;
|
||||
use function Shlinkio\Shlink\Config\loadConfigFromGlob;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -23,19 +24,20 @@ class ConfigProvider
|
|||
public static function applyRoutesPrefix(array $routes): array
|
||||
{
|
||||
$healthRoute = self::buildUnversionedHealthRouteFromExistingRoutes($routes);
|
||||
$prefixedRoutes = map($routes, static function (array $route) {
|
||||
$prefixedRoutes = array_map(static function (array $route) {
|
||||
['path' => $path] = $route;
|
||||
$route['path'] = sprintf('%s%s', self::ROUTES_PREFIX, $path);
|
||||
return $route;
|
||||
});
|
||||
}, $routes);
|
||||
|
||||
return $healthRoute !== null ? [...$prefixedRoutes, $healthRoute] : $prefixedRoutes;
|
||||
}
|
||||
|
||||
private static function buildUnversionedHealthRouteFromExistingRoutes(array $routes): ?array
|
||||
{
|
||||
$healthRoute = first($routes, fn (array $route) => $route['path'] === '/health');
|
||||
if ($healthRoute === null) {
|
||||
$healthRoutes = array_filter($routes, fn (array $route) => $route['path'] === '/health');
|
||||
$healthRoute = reset($healthRoutes);
|
||||
if ($healthRoute === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
|||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
|
||||
use function end;
|
||||
use function explode;
|
||||
use function Functional\last;
|
||||
|
||||
/** @deprecated */
|
||||
class BackwardsCompatibleProblemDetailsException extends RuntimeException implements ProblemDetailsExceptionInterface
|
||||
|
@ -77,7 +77,9 @@ class BackwardsCompatibleProblemDetailsException extends RuntimeException implem
|
|||
|
||||
private function remapType(string $wrappedType): string
|
||||
{
|
||||
$lastSegment = last(explode('/', $wrappedType));
|
||||
$segments = explode('/', $wrappedType);
|
||||
$lastSegment = end($segments);
|
||||
|
||||
return match ($lastSegment) {
|
||||
ValidationException::ERROR_CODE => 'INVALID_ARGUMENT',
|
||||
DeleteShortUrlException::ERROR_CODE => 'INVALID_SHORT_URL_DELETION',
|
||||
|
|
|
@ -17,16 +17,16 @@ use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
|||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface
|
||||
{
|
||||
public const API_KEY_HEADER = 'X-Api-Key';
|
||||
|
||||
public function __construct(
|
||||
private ApiKeyServiceInterface $apiKeyService,
|
||||
private array $routesWithoutApiKey,
|
||||
private array $routesWithQueryApiKey,
|
||||
private readonly ApiKeyServiceInterface $apiKeyService,
|
||||
private readonly array $routesWithoutApiKey,
|
||||
private readonly array $routesWithQueryApiKey,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
|
|||
$routeResult === null
|
||||
|| $routeResult->isFailure()
|
||||
|| $request->getMethod() === self::METHOD_OPTIONS
|
||||
|| contains($this->routesWithoutApiKey, $routeResult->getMatchedRouteName())
|
||||
|| contains($routeResult->getMatchedRouteName(), $this->routesWithoutApiKey)
|
||||
) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
|
|||
{
|
||||
$routeName = $routeResult->getMatchedRouteName();
|
||||
$query = $request->getQueryParams();
|
||||
$isRouteWithApiKeyInQuery = contains($this->routesWithQueryApiKey, $routeName);
|
||||
$isRouteWithApiKeyInQuery = contains($routeName, $this->routesWithQueryApiKey);
|
||||
$apiKey = $isRouteWithApiKeyInQuery ? ($query['apiKey'] ?? '') : $request->getHeaderLine(self::API_KEY_HEADER);
|
||||
|
||||
if (empty($apiKey)) {
|
||||
|
|
|
@ -12,7 +12,7 @@ use Psr\Http\Server\MiddlewareInterface;
|
|||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\MalformedBodyException;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Json\json_decode;
|
||||
|
||||
class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
|
||||
|
@ -25,11 +25,11 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
|
|||
// In requests that do not allow body or if the body has already been parsed, continue to next middleware
|
||||
if (
|
||||
! empty($currentParams)
|
||||
|| contains([
|
||||
|| contains($method, [
|
||||
self::METHOD_GET,
|
||||
self::METHOD_HEAD,
|
||||
self::METHOD_OPTIONS,
|
||||
], $method)
|
||||
])
|
||||
) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
|||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function range;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -108,7 +108,7 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
|
||||
public static function provideMaxVisits(): array
|
||||
{
|
||||
return map(range(10, 15), fn(int $i) => [$i]);
|
||||
return array_map(static fn (int $i) => [$i], range(10, 15));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
Loading…
Add table
Reference in a new issue