From f50263d2d992f127fe796b6b3b21ebb0e13928a1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 29 Nov 2023 12:34:13 +0100 Subject: [PATCH 1/4] Remove usage of Functional\map function --- docker-compose.yml | 2 +- .../CLI/src/Command/Api/ListKeysCommand.php | 6 ++-- .../src/Command/Db/CreateDatabaseCommand.php | 4 +-- .../src/Command/Domain/ListDomainsCommand.php | 8 ++--- .../Command/ShortUrl/ListShortUrlsCommand.php | 8 ++--- .../CLI/src/Command/Tag/ListTagsCommand.php | 8 ++--- .../Visit/AbstractVisitsListCommand.php | 24 ++++++++++---- module/CLI/test/ApiKey/RoleResolverTest.php | 11 ++++--- .../test/GeoLite/GeolocationDbUpdaterTest.php | 4 +-- module/Core/functions/functions.php | 4 +-- .../Config/PostProcessor/BasePathPrefixer.php | 6 ++-- .../MultiSegmentSlugProcessor.php | 6 ++-- module/Core/src/Domain/DomainService.php | 4 +-- .../EventDispatcher/NotifyVisitToWebHooks.php | 18 +++++----- module/Core/src/ShortUrl/Entity/ShortUrl.php | 6 ++-- .../src/ShortUrl/Model/DeviceLongUrlPair.php | 26 +++++++++------ .../PersistenceShortUrlRelationResolver.php | 26 +++++++++------ .../SimpleShortUrlRelationResolver.php | 4 +-- .../Transformer/ShortUrlDataTransformer.php | 10 +++--- .../Core/src/Tag/Repository/TagRepository.php | 6 ++-- module/Core/src/Visit/RequestTracker.php | 22 +++++++++---- .../Repository/ShortUrlListRepositoryTest.php | 10 +++--- .../Adapter/TagsPaginatorAdapterTest.php | 4 +-- .../VisitLocationRepositoryTest.php | 4 +-- .../EventDispatcher/UpdateGeoLiteDbTest.php | 6 ++-- .../Exception/DeleteShortUrlExceptionTest.php | 6 ++-- module/Core/test/Functions/FunctionsTest.php | 13 +++++--- .../ShortUrl/DeleteShortUrlServiceTest.php | 4 +-- .../test/ShortUrl/Entity/ShortUrlTest.php | 4 +-- .../test/ShortUrl/ShortUrlResolverTest.php | 10 +++--- .../ShortUrlDataTransformerTest.php | 10 ++++++ .../Visit/Geolocation/VisitLocatorTest.php | 6 ++-- .../Core/test/Visit/VisitsStatsHelperTest.php | 33 ++++++++++++++----- module/Rest/config/access-logs.config.php | 4 +-- module/Rest/src/Action/Tag/ListTagsAction.php | 4 +-- module/Rest/src/ConfigProvider.php | 6 ++-- .../test-api/Action/CreateShortUrlTest.php | 4 +-- 37 files changed, 201 insertions(+), 140 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e44ca82b7..f33693ad7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/module/CLI/src/Command/Api/ListKeysCommand.php b/module/CLI/src/Command/Api/ListKeysCommand.php index 4fd4b0050..b55dcd7d9 100644 --- a/module/CLI/src/Command/Api/ListKeysCommand.php +++ b/module/CLI/src/Command/Api/ListKeysCommand.php @@ -15,7 +15,7 @@ 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 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int { $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 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int )); return $rowData; - }); + }, $this->apiKeyService->listKeys($enabledOnly)); ShlinkTable::withRowSeparators($output)->render(array_filter([ 'Key', diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php index 129db1e0b..c70e2f76f 100644 --- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php +++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php @@ -16,8 +16,8 @@ use Symfony\Component\Process\PhpExecutableFinder; use Throwable; +use function array_map; use function Functional\contains; -use function Functional\map; use function Functional\some; class CreateDatabaseCommand extends AbstractDatabaseCommand @@ -70,7 +70,7 @@ private function databaseTablesExist(): bool { $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. diff --git a/module/CLI/src/Command/Domain/ListDomainsCommand.php b/module/CLI/src/Command/Domain/ListDomainsCommand.php index 11a0f5b9f..501072928 100644 --- a/module/CLI/src/Command/Domain/ListDomainsCommand.php +++ b/module/CLI/src/Command/Domain/ListDomainsCommand.php @@ -14,13 +14,13 @@ 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 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int $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 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int $this->notFoundRedirectsToString($domain->notFoundRedirectConfig), ] : $commonValues; - }), + }, $domains), ); return ExitCode::EXIT_SUCCESS; diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index 14ea18515..c9497daf2 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -23,9 +23,9 @@ 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 @@ private function renderPage( ): 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), diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php index 41ca9b601..d56e4101a 100644 --- a/module/CLI/src/Command/Tag/ListTagsCommand.php +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -13,13 +13,13 @@ 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 @@ private function getTagsRows(): array return [['No tags found', '-', '-']]; } - return map( - $tags, + return array_map( static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsSummary->total], + [...$tags], ); } } diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php index ba5186564..a247380e1 100644 --- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -16,12 +16,15 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function array_filter; use function array_keys; -use function Functional\map; -use function Functional\select_keys; +use function array_map; +use function in_array; use function Shlinkio\Shlink\Common\buildDateRange; use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly; +use const ARRAY_FILTER_USE_KEY; + abstract class AbstractVisitsListCommand extends Command { private readonly StartDateOption $startDateOption; @@ -49,7 +52,7 @@ final protected function execute(InputInterface $input, OutputInterface $output) 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 +63,18 @@ private function resolveRowsAndHeaders(Paginator $paginator): array ...$extraFields, ]; - return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); - }); - $extra = map($extraKeys, camelCaseToHumanFriendly(...)); + // Filter out unknown keys + return array_filter( + $rowData, + static fn (string $key) => in_array( + $key, + ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys], + strict: true, + ), + ARRAY_FILTER_USE_KEY, + ); + }, [...$paginator->getCurrentPageResults()]); + $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); return [ $rows, diff --git a/module/CLI/test/ApiKey/RoleResolverTest.php b/module/CLI/test/ApiKey/RoleResolverTest.php index 7aecda6d2..cbd4f0fa1 100644 --- a/module/CLI/test/ApiKey/RoleResolverTest.php +++ b/module/CLI/test/ApiKey/RoleResolverTest.php @@ -16,8 +16,6 @@ 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 @@ public static function provideRoles(): iterable { $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; }; diff --git a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php index 9d32ca79e..0f911db8f 100644 --- a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php +++ b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php @@ -21,7 +21,7 @@ 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 @@ public static function provideSmallDays(): iterable return [$days % 2 === 0 ? $timestamp : (string) $timestamp]; }; - return map(range(0, 34), $generateParamsWithTimestamp); + return array_map($generateParamsWithTimestamp, range(0, 34)); } #[Test] diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index b6acbb357..32d357e34 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -17,8 +17,8 @@ use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode; +use function array_map; use function date_default_timezone_get; -use function Functional\map; use function Functional\reduce_left; use function is_array; use function print_r; @@ -177,6 +177,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()) ); } diff --git a/module/Core/src/Config/PostProcessor/BasePathPrefixer.php b/module/Core/src/Config/PostProcessor/BasePathPrefixer.php index 619e60563..616759f18 100644 --- a/module/Core/src/Config/PostProcessor/BasePathPrefixer.php +++ b/module/Core/src/Config/PostProcessor/BasePathPrefixer.php @@ -4,7 +4,7 @@ namespace Shlinkio\Shlink\Core\Config\PostProcessor; -use function Functional\map; +use function array_map; class BasePathPrefixer { @@ -23,13 +23,13 @@ public function __invoke(array $config): array 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] ?? []); } } diff --git a/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php b/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php index 339450636..585f78b6b 100644 --- a/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php +++ b/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php @@ -4,7 +4,7 @@ namespace Shlinkio\Shlink\Core\Config\PostProcessor; -use function Functional\map; +use function array_map; use function str_replace; class MultiSegmentSlugProcessor @@ -19,11 +19,11 @@ public function __invoke(array $config): array 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; } diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php index 703f77fd2..9aa4e3d06 100644 --- a/module/Core/src/Domain/DomainService.php +++ b/module/Core/src/Domain/DomainService.php @@ -14,9 +14,9 @@ use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\Entity\ApiKey; +use function array_map; use function Functional\first; use function Functional\group; -use function Functional\map; class DomainService implements DomainServiceInterface { @@ -30,7 +30,7 @@ public function __construct(private readonly EntityManagerInterface $em, private 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; diff --git a/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php b/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php index 317821b1c..028c3c134 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php @@ -19,18 +19,18 @@ 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 @@ private function buildRequestOptions(Visit $visit): array */ 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(), ); } diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php index 8fbec5ed5..e53e9afaf 100644 --- a/module/Core/src/ShortUrl/Entity/ShortUrl.php +++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php @@ -27,8 +27,8 @@ 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 @@ public static function create( $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; diff --git a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php index d017c7e5d..c7b1efc06 100644 --- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php +++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php @@ -6,9 +6,7 @@ use Shlinkio\Shlink\Core\Model\DeviceType; -use function array_values; use function Functional\group; -use function Functional\map; use function trim; final class DeviceLongUrlPair @@ -32,15 +30,23 @@ public static function fromRawTypeAndLongUrl(string $type, string $longUrl): sel */ public static function fromMapToChangeSet(array $map): array { + $toRemove = []; // TODO Use when group is removed + $toKeep = []; // TODO Use when group is removed $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), - ); + + $deviceTypesToRemove = []; + foreach ($typesWithNullUrl['remove'] ?? [] as $deviceType => $_) { + $deviceTypesToRemove[] = DeviceType::from($deviceType); + } + + $pairsToKeep = []; + /** + * @var string $deviceType + * @var string $longUrl + */ + foreach ($typesWithNullUrl['keep'] ?? [] as $deviceType => $longUrl) { + $pairsToKeep[$deviceType] = self::fromRawTypeAndLongUrl($deviceType, $longUrl); + } return [$pairsToKeep, $deviceTypesToRemove]; } diff --git a/module/Core/src/ShortUrl/Resolver/PersistenceShortUrlRelationResolver.php b/module/Core/src/ShortUrl/Resolver/PersistenceShortUrlRelationResolver.php index 17669f322..6c49ab5f9 100644 --- a/module/Core/src/ShortUrl/Resolver/PersistenceShortUrlRelationResolver.php +++ b/module/Core/src/ShortUrl/Resolver/PersistenceShortUrlRelationResolver.php @@ -15,9 +15,8 @@ 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 @@ public function resolveTags(array $tags): Collections\Collection 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 @@ public function resolveTags(array $tags): Collections\Collection $this->em->persist($tag); return $tag; - })); + }, $tags)); } private function memoizeNewTag(string $tagName): Tag @@ -110,6 +109,7 @@ private function lock(array &$locks, string $name): void $lock->acquire(true); } + /** /** * @param array $locks */ @@ -126,9 +126,15 @@ public function postFlush(): void $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 = []; } } diff --git a/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php b/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php index 609a300c2..c1a9d0ab3 100644 --- a/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php +++ b/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php @@ -8,7 +8,7 @@ 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 @@ public function resolveDomain(?string $domain): ?Domain */ 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)); } } diff --git a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php index 9de5c4084..a66419984 100644 --- a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php +++ b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php @@ -7,10 +7,10 @@ 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 @@ public function transform($shortUrl): array // phpcs:ignore '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 @@ private function buildMeta(ShortUrl $shortUrl): array $maxVisits = $shortUrl->getMaxVisits(); return [ - 'validSince' => invoke_if($validSince, 'toAtomString'), - 'validUntil' => invoke_if($validUntil, 'toAtomString'), + 'validSince' => $validSince?->toAtomString(), + 'validUntil' => $validUntil?->toAtomString(), 'maxVisits' => $maxVisits, ]; } diff --git a/module/Core/src/Tag/Repository/TagRepository.php b/module/Core/src/Tag/Repository/TagRepository.php index 278dbe8b5..d74da44ae 100644 --- a/module/Core/src/Tag/Repository/TagRepository.php +++ b/module/Core/src/Tag/Repository/TagRepository.php @@ -17,8 +17,8 @@ use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin; use Shlinkio\Shlink\Rest\Entity\ApiKey; +use function array_map; use function Functional\each; -use function Functional\map; use function Shlinkio\Shlink\Core\camelCaseToSnakeCase; use const PHP_INT_MAX; @@ -126,9 +126,9 @@ public function findTagsWithInfo(?TagsListFiltering $filtering = null): array $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(), ); } diff --git a/module/Core/src/Visit/RequestTracker.php b/module/Core/src/Visit/RequestTracker.php index cb43e10dd..e86471658 100644 --- a/module/Core/src/Visit/RequestTracker.php +++ b/module/Core/src/Visit/RequestTracker.php @@ -16,9 +16,9 @@ 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 str_contains; @@ -85,22 +85,30 @@ private function shouldDisableTrackingFromAddress(?string $remoteAddr): bool $remoteAddrParts = explode('.', $remoteAddr); $disableTrackingFrom = $this->trackingOptions->disableTrackingFrom; - return some($disableTrackingFrom, function (string $value) use ($ip, $remoteAddrParts): bool { + foreach ($disableTrackingFrom as $value) { $range = str_contains($value, '*') ? $this->parseValueWithWildcards($value, $remoteAddrParts) : Factory::parseRangeString($value); - return $range !== null && $ip->matches($range); - }); + if ($range !== null && $ip->matches($range)) { + return true; + } + } + + return false; } 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, )), ); } diff --git a/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php b/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php index 46c08d25f..b359e35d5 100644 --- a/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php @@ -22,8 +22,8 @@ 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 @@ public function findListProperlyFiltersResult(): void $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'); diff --git a/module/Core/test-db/Tag/Paginator/Adapter/TagsPaginatorAdapterTest.php b/module/Core/test-db/Tag/Paginator/Adapter/TagsPaginatorAdapterTest.php index 0dd833418..f88a8e7f5 100644 --- a/module/Core/test-db/Tag/Paginator/Adapter/TagsPaginatorAdapterTest.php +++ b/module/Core/test-db/Tag/Paginator/Adapter/TagsPaginatorAdapterTest.php @@ -12,7 +12,7 @@ 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 @@ public function expectedListOfTagsIsReturned( '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()); diff --git a/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php index 79c80a24f..c5aadf1f6 100644 --- a/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php @@ -14,7 +14,7 @@ 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 @@ public function findVisitsReturnsProperVisits(int $blockSize): void 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)); } } diff --git a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php index 6ba20ec87..dc6045218 100644 --- a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php +++ b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php @@ -16,7 +16,7 @@ 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 @@ public function dispatchesEventOnlyWhenDbFileHasBeenCreatedForTheFirstTime( 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()); } } diff --git a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php index 8d82c11ee..c1b2bcecd 100644 --- a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php +++ b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php @@ -10,7 +10,7 @@ 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 @@ public function fromVisitsThresholdGeneratesMessageProperly( 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] diff --git a/module/Core/test/Functions/FunctionsTest.php b/module/Core/test/Functions/FunctionsTest.php index 3f6026a0e..715685af7 100644 --- a/module/Core/test/Functions/FunctionsTest.php +++ b/module/Core/test/Functions/FunctionsTest.php @@ -13,7 +13,7 @@ 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 @@ public function enumValuesReturnsExpectedValueForEnum(string $enum, array $expec 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()), ]; } } diff --git a/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php index 65351a931..3ac9897c2 100644 --- a/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php +++ b/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php @@ -18,7 +18,7 @@ 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(); diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index bd83fd9a5..c1d66e619 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -19,8 +19,8 @@ use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; use Shlinkio\Shlink\Importer\Sources\ImportSource; +use function array_map; use function Functional\every; -use function Functional\map; use function range; use function strlen; use function strtolower; @@ -88,7 +88,7 @@ public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): 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] diff --git a/module/Core/test/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/ShortUrl/ShortUrlResolverTest.php index 4057691b3..a95426ba1 100644 --- a/module/Core/test/ShortUrl/ShortUrlResolverTest.php +++ b/module/Core/test/ShortUrl/ShortUrlResolverTest.php @@ -25,7 +25,7 @@ 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 @@ public static function provideDisabledShortUrls(): iterable $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 @@ public static function provideDisabledShortUrls(): iterable '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; diff --git a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php index 27916063e..349392dbd 100644 --- a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php +++ b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php @@ -84,4 +84,14 @@ public static function provideShortUrls(): iterable ], ]; } + + #[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); + } } diff --git a/module/Core/test/Visit/Geolocation/VisitLocatorTest.php b/module/Core/test/Visit/Geolocation/VisitLocatorTest.php index 70fc62431..1d3af228b 100644 --- a/module/Core/test/Visit/Geolocation/VisitLocatorTest.php +++ b/module/Core/test/Visit/Geolocation/VisitLocatorTest.php @@ -20,9 +20,9 @@ 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 @@ public function locateVisitsIteratesAndLocatesExpectedVisits( 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); diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php index d43efc240..dd11fdef5 100644 --- a/module/Core/test/Visit/VisitsStatsHelperTest.php +++ b/module/Core/test/Visit/VisitsStatsHelperTest.php @@ -33,8 +33,8 @@ 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 @@ function (VisitsCountFiltering $options) use ($expectedCount, $apiKey, &$callCou 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 @@ public function infoReturnsVisitsForCertainShortCode(?ApiKey $apiKey): void $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 @@ public function visitsForTagAreReturnedAsExpected(?ApiKey $apiKey): void $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 @@ public function visitsForNonDefaultDomainAreReturnedAsExpected(?ApiKey $apiKey): $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 @@ public function visitsForDefaultDomainAreReturnedAsExpected(?ApiKey $apiKey): vo $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 @@ public function visitsForDefaultDomainAreReturnedAsExpected(?ApiKey $apiKey): vo #[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 @@ public function orphanVisitsAreReturnedAsExpected(): void #[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), diff --git a/module/Rest/config/access-logs.config.php b/module/Rest/config/access-logs.config.php index 1f0dd0e8c..def1a93a5 100644 --- a/module/Rest/config/access-logs.config.php +++ b/module/Rest/config/access-logs.config.php @@ -11,7 +11,7 @@ return [ 'access_logs' => [ - 'ignored_paths' => [ + 'ignored_path_prefixes' => [ Action\HealthAction::ROUTE_PATH, ], ], @@ -20,7 +20,7 @@ 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'], ), ], diff --git a/module/Rest/src/Action/Tag/ListTagsAction.php b/module/Rest/src/Action/Tag/ListTagsAction.php index 34f44475b..9674d5bc5 100644 --- a/module/Rest/src/Action/Tag/ListTagsAction.php +++ b/module/Rest/src/Action/Tag/ListTagsAction.php @@ -14,7 +14,7 @@ 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 @@ public function handle(ServerRequestInterface $request): ResponseInterface // 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]); } diff --git a/module/Rest/src/ConfigProvider.php b/module/Rest/src/ConfigProvider.php index 215a4d6e5..7c57d8b19 100644 --- a/module/Rest/src/ConfigProvider.php +++ b/module/Rest/src/ConfigProvider.php @@ -4,8 +4,8 @@ namespace Shlinkio\Shlink\Rest; +use function array_map; use function Functional\first; -use function Functional\map; use function Shlinkio\Shlink\Config\loadConfigFromGlob; use function sprintf; @@ -23,11 +23,11 @@ public function __invoke(): array 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; } diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php index 78f738a37..01592129a 100644 --- a/module/Rest/test-api/Action/CreateShortUrlTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlTest.php @@ -10,7 +10,7 @@ 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 @@ public function createsNewShortUrlWithVisitsLimit(int $maxVisits): void 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] From 549c6605f0c2ebab0573f632df6d553a184ac342 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Nov 2023 09:13:29 +0100 Subject: [PATCH 2/4] Replaced usage of Functional\contians --- composer.json | 8 +++--- config/autoload/entity-manager.global.php | 4 +-- config/test/test_config.global.php | 4 +-- .../src/Command/Db/CreateDatabaseCommand.php | 15 +++++++---- .../ShortUrl/CreateShortUrlCommand.php | 9 +++---- .../Visit/AbstractVisitsListCommand.php | 5 ++-- module/Core/functions/functions.php | 25 ++++++++++++++++--- module/Core/src/Action/Model/QrCodeParams.php | 4 +-- .../src/ShortUrl/Model/OrderableField.php | 4 +-- .../Validation/DeviceLongUrlsValidator.php | 4 +-- module/Core/src/Util/RedirectStatus.php | 6 ++--- .../NotifyVisitToWebHooksTest.php | 4 +-- .../Importer/ImportedLinksProcessorTest.php | 4 +-- .../Middleware/AuthenticationMiddleware.php | 12 ++++----- .../src/Middleware/BodyParserMiddleware.php | 6 ++--- 15 files changed, 68 insertions(+), 46 deletions(-) diff --git a/composer.json b/composer.json index 0e1b996e9..8071556ec 100644 --- a/composer.json +++ b/composer.json @@ -46,12 +46,12 @@ "php-middleware/request-id": "^4.1", "pugx/shortid-php": "^1.1", "ramsey/uuid": "^4.7", - "shlinkio/shlink-common": "^5.7", + "shlinkio/shlink-common": "dev-main#1f1b3b8 as 5.8", "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": "dev-main#4616c54 as 5.3", + "shlinkio/shlink-installer": "dev-develop#cb0eaea as 8.7", + "shlinkio/shlink-ip-geolocation": "dev-main#ea88ae8 as 3.4", "shlinkio/shlink-json": "^1.1", "spiral/roadrunner": "^2023.2", "spiral/roadrunner-cli": "^2.5", diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php index 588992179..440956560 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -5,11 +5,11 @@ use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Shlinkio\Shlink\Core\Config\EnvVars; -use function Functional\contains; +use function Shlinkio\Shlink\Core\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', diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 1beed0e32..75937bece 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -28,9 +28,9 @@ 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\contains; use function sprintf; use function sys_get_temp_dir; @@ -41,7 +41,7 @@ $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) { diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php index c70e2f76f..b9bb8f104 100644 --- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php +++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php @@ -17,8 +17,7 @@ use Throwable; use function array_map; -use function Functional\contains; -use function Functional\some; +use function Shlinkio\Shlink\Core\contains; class CreateDatabaseCommand extends AbstractDatabaseCommand { @@ -72,9 +71,15 @@ private function databaseTablesExist(): bool $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); $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)); + foreach ($shlinkTables as $shlinkTable) { + // 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. + if (contains($shlinkTable, $existingTables)) { + return true; + } + } + + return false; } private function ensureDatabaseExistsAndGetTables(): array diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index f55f247d6..3277f7634 100644 --- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -20,10 +20,9 @@ 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\flatten; use function sprintf; class CreateShortUrlCommand extends Command @@ -144,8 +143,8 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int 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; diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php index a247380e1..8766ecc55 100644 --- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -19,9 +19,9 @@ use function array_filter; use function array_keys; use function array_map; -use function in_array; use function Shlinkio\Shlink\Common\buildDateRange; use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly; +use function Shlinkio\Shlink\Core\contains; use const ARRAY_FILTER_USE_KEY; @@ -66,10 +66,9 @@ private function resolveRowsAndHeaders(Paginator $paginator): array // Filter out unknown keys return array_filter( $rowData, - static fn (string $key) => in_array( + static fn (string $key) => contains( $key, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys], - strict: true, ), ARRAY_FILTER_USE_KEY, ); diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 32d357e34..bcda4bb46 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -6,7 +6,6 @@ use BackedEnum; use Cake\Chronos\Chronos; -use Cake\Chronos\ChronosInterface; use DateTimeInterface; use Doctrine\ORM\Mapping\Builder\FieldBuilder; use Jaybizzle\CrawlerDetect\CrawlerDetect; @@ -18,8 +17,10 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode; use function array_map; +use function array_reduce; use function date_default_timezone_get; use function Functional\reduce_left; +use function in_array; use function is_array; use function print_r; use function Shlinkio\Shlink\Common\buildDateRange; @@ -57,7 +58,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 +69,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); } @@ -180,3 +181,21 @@ function enumValues(string $enum): array $cache[$enum] = array_map(static fn (BackedEnum $type) => (string) $type->value, $enum::cases()) ); } + +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: [], + ); +} diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 306c2b444..51162d5f3 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -18,7 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Options\QrCodeOptions; -use function Functional\contains; +use function Shlinkio\Shlink\Core\contains; use function strtolower; use function trim; @@ -74,7 +74,7 @@ private static function resolveMargin(array $query, QrCodeOptions $defaults): in 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(), diff --git a/module/Core/src/ShortUrl/Model/OrderableField.php b/module/Core/src/ShortUrl/Model/OrderableField.php index ac1bc632f..1b61a1557 100644 --- a/module/Core/src/ShortUrl/Model/OrderableField.php +++ b/module/Core/src/ShortUrl/Model/OrderableField.php @@ -2,7 +2,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model; -use function Functional\contains; +use function Shlinkio\Shlink\Core\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], ); } diff --git a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php index 9fda18098..5694f6e14 100644 --- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php +++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php @@ -10,9 +10,9 @@ 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\contains; use function Shlinkio\Shlink\Core\enumValues; class DeviceLongUrlsValidator extends AbstractValidator @@ -41,7 +41,7 @@ public function isValid(mixed $value): bool $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; } diff --git a/module/Core/src/Util/RedirectStatus.php b/module/Core/src/Util/RedirectStatus.php index 76c047f4a..313dc4325 100644 --- a/module/Core/src/Util/RedirectStatus.php +++ b/module/Core/src/Util/RedirectStatus.php @@ -2,7 +2,7 @@ namespace Shlinkio\Shlink\Core\Util; -use function Functional\contains; +use function Shlinkio\Shlink\Core\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]); } } diff --git a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php index f85a9d440..c4ca402a3 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php @@ -28,7 +28,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor; use function count; -use function Functional\contains; +use function Shlinkio\Shlink\Core\contains; class NotifyVisitToWebHooksTest extends TestCase { @@ -102,7 +102,7 @@ public function expectedRequestsArePerformedToWebhooks(Visit $visit, array $expe 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( diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php index bf2896e2c..5b1740534 100644 --- a/module/Core/test/Importer/ImportedLinksProcessorTest.php +++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php @@ -32,8 +32,8 @@ use Symfony\Component\Console\Style\StyleInterface; use function count; -use function Functional\contains; use function Functional\some; +use function Shlinkio\Shlink\Core\contains; use function sprintf; use function str_contains; @@ -128,8 +128,8 @@ public function alreadyImportedUrlsAreSkipped(): void $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); diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php index 7b9118173..85ec61b7c 100644 --- a/module/Rest/src/Middleware/AuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php @@ -17,16 +17,16 @@ use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; -use function Functional\contains; +use function Shlinkio\Shlink\Core\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 @@ public function process(Request $request, RequestHandlerInterface $handler): Res $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 @@ private function getApiKeyFromRequest(ServerRequestInterface $request, RouteResu { $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)) { diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php index c31bc268e..b0548f975 100644 --- a/module/Rest/src/Middleware/BodyParserMiddleware.php +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -12,7 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Exception\MalformedBodyException; -use function Functional\contains; +use function Shlinkio\Shlink\Core\contains; use function Shlinkio\Shlink\Json\json_decode; class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface @@ -25,11 +25,11 @@ public function process(Request $request, RequestHandlerInterface $handler): Res // 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); } From bff4bd12ae08038bb6223bde0f8514d5bcb2650f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Nov 2023 14:34:21 +0100 Subject: [PATCH 3/4] Removed more functional-php usages --- composer.json | 3 ++ data/migrations/Version20200105165647.php | 9 ++--- data/migrations/Version20200106215144.php | 19 ++++++---- data/migrations/Version20200110182849.php | 13 +++---- .../Command/Domain/DomainRedirectsCommand.php | 12 +++---- module/CLI/src/Util/ShlinkTable.php | 28 +++++++++++---- module/Core/functions/functions.php | 36 +++++++++++++++++-- .../src/Config/NotFoundRedirectResolver.php | 7 ++-- .../ShortUrlMethodsProcessor.php | 29 +++++++++------ module/Core/src/Domain/DomainService.php | 19 ++++++---- .../Async/AbstractNotifyVisitListener.php | 4 +-- .../src/ShortUrl/Model/DeviceLongUrlPair.php | 24 +++++-------- .../Validation/DeviceLongUrlsValidator.php | 2 +- .../Core/src/Tag/Repository/TagRepository.php | 5 +-- .../RabbitMq/NotifyVisitToRabbitMqTest.php | 9 +++-- .../Importer/ImportedLinksProcessorTest.php | 2 +- .../test/ShortUrl/Entity/ShortUrlTest.php | 2 +- .../TrimTrailingSlashMiddlewareTest.php | 8 ++--- module/Rest/src/ConfigProvider.php | 8 +++-- ...wardsCompatibleProblemDetailsException.php | 6 ++-- 20 files changed, 155 insertions(+), 90 deletions(-) diff --git a/composer.json b/composer.json index 8071556ec..e295539ef 100644 --- a/composer.json +++ b/composer.json @@ -80,6 +80,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", diff --git a/data/migrations/Version20200105165647.php b/data/migrations/Version20200105165647.php index fb3b79617..bb4970219 100644 --- a/data/migrations/Version20200105165647.php +++ b/data/migrations/Version20200105165647.php @@ -11,7 +11,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; -use function Functional\some; +use function Shlinkio\Shlink\Core\some; final class Version20200105165647 extends AbstractMigration { @@ -23,11 +23,12 @@ final class Version20200105165647 extends AbstractMigration public function preUp(Schema $schema): void { $visitLocations = $schema->getTable('visit_locations'); - $this->skipIf(some( - self::COLUMNS, - fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName), + $this->skipIf(some( + self::COLUMNS, + fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName), ), 'New columns already exist'); + foreach (self::COLUMNS as $columnName) { $qb = $this->connection->createQueryBuilder(); $qb->update('visit_locations') diff --git a/data/migrations/Version20200106215144.php b/data/migrations/Version20200106215144.php index 830daf646..f5faba4ed 100644 --- a/data/migrations/Version20200106215144.php +++ b/data/migrations/Version20200106215144.php @@ -7,11 +7,10 @@ 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 */ diff --git a/data/migrations/Version20200110182849.php b/data/migrations/Version20200110182849.php index b267bfbc3..4b608bb20 100644 --- a/data/migrations/Version20200110182849.php +++ b/data/migrations/Version20200110182849.php @@ -9,9 +9,6 @@ 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); + } + } } /** diff --git a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php index 4a3f8062b..bf08e7f3d 100644 --- a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php +++ b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php @@ -14,8 +14,8 @@ 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 @@ protected function interact(InputInterface $input, OutputInterface $output): voi $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()); diff --git a/module/CLI/src/Util/ShlinkTable.php b/module/CLI/src/Util/ShlinkTable.php index cd38e5cd0..c421c6131 100644 --- a/module/CLI/src/Util/ShlinkTable.php +++ b/module/CLI/src/Util/ShlinkTable.php @@ -8,30 +8,30 @@ 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 = ' %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 @@ public function render(array $headers, array $rows, ?string $footerTitle = null, $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 @@ public function render(array $headers, array $rows, ?string $footerTitle = null, ->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; + } } diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index bcda4bb46..f13fc670f 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -16,10 +16,10 @@ 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\reduce_left; use function in_array; use function is_array; use function print_r; @@ -95,10 +95,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", @@ -199,3 +201,33 @@ function flatten(array $multiArray): array initial: [], ); } + +/** + * Checks if a callback returns true for at least one item in a collection. + * @param callable(mixed $value, string|number $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; +} diff --git a/module/Core/src/Config/NotFoundRedirectResolver.php b/module/Core/src/Config/NotFoundRedirectResolver.php index 3ab2e7405..540956ee7 100644 --- a/module/Core/src/Config/NotFoundRedirectResolver.php +++ b/module/Core/src/Config/NotFoundRedirectResolver.php @@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface; use function Functional\compose; -use function Functional\id; use function str_replace; use function urlencode; @@ -23,8 +22,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, ) { } @@ -73,7 +72,7 @@ private function resolvePlaceholders(UriInterface $currentUri, string $redirectU $replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier), ); $replacePlaceholdersInPath = compose( - $replacePlaceholders(id(...)), + $replacePlaceholders(static fn (mixed $v) => $v), static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), ); $replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...)); diff --git a/module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php b/module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php index 05ecdb6c9..42f00889d 100644 --- a/module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php +++ b/module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php @@ -9,25 +9,34 @@ 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; diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php index 9aa4e3d06..93adbf5f1 100644 --- a/module/Core/src/Domain/DomainService.php +++ b/module/Core/src/Domain/DomainService.php @@ -15,8 +15,6 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; use function array_map; -use function Functional\first; -use function Functional\group; class DomainService implements DomainServiceInterface { @@ -49,12 +47,19 @@ private function defaultDomainAndRest(?ApiKey $apiKey): array { /** @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 = []; + + foreach ($allDomains as $domain) { + if ($domain->authority === $this->defaultDomain) { + $defaultDomain = $domain; + } else { + $restOfDomains[] = $domain; + } + } - return [first($groups['default'] ?? []), $groups['domains'] ?? []]; + return [$defaultDomain, $restOfDomains]; } /** diff --git a/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php b/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php index dae9130fa..3ec9417cc 100644 --- a/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php +++ b/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php @@ -13,7 +13,7 @@ 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 @@ public function __invoke(VisitLocated $visitLocated): void $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}', diff --git a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php index c7b1efc06..a83ec01d9 100644 --- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php +++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php @@ -6,7 +6,6 @@ use Shlinkio\Shlink\Core\Model\DeviceType; -use function Functional\group; use function trim; final class DeviceLongUrlPair @@ -25,27 +24,20 @@ public static function fromRawTypeAndLongUrl(string $type, string $longUrl): sel * * 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 $map + * @param array $map * @return array{array, DeviceType[]} */ public static function fromMapToChangeSet(array $map): array { - $toRemove = []; // TODO Use when group is removed - $toKeep = []; // TODO Use when group is removed - $typesWithNullUrl = group($map, static fn (?string $longUrl) => $longUrl === null ? 'remove' : 'keep'); - + $pairsToKeep = []; $deviceTypesToRemove = []; - foreach ($typesWithNullUrl['remove'] ?? [] as $deviceType => $_) { - $deviceTypesToRemove[] = DeviceType::from($deviceType); - } - $pairsToKeep = []; - /** - * @var string $deviceType - * @var string $longUrl - */ - foreach ($typesWithNullUrl['keep'] ?? [] as $deviceType => $longUrl) { - $pairsToKeep[$deviceType] = self::fromRawTypeAndLongUrl($deviceType, $longUrl); + foreach ($map as $deviceType => $longUrl) { + if ($longUrl === null) { + $deviceTypesToRemove[] = DeviceType::from($deviceType); + } else { + $pairsToKeep[$deviceType] = self::fromRawTypeAndLongUrl($deviceType, $longUrl); + } } return [$pairsToKeep, $deviceTypesToRemove]; diff --git a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php index 5694f6e14..0c3b19c24 100644 --- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php +++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php @@ -10,10 +10,10 @@ use function array_keys; use function array_values; -use function Functional\every; use function is_array; use function Shlinkio\Shlink\Core\contains; use function Shlinkio\Shlink\Core\enumValues; +use function Shlinkio\Shlink\Core\every; class DeviceLongUrlsValidator extends AbstractValidator { diff --git a/module/Core/src/Tag/Repository/TagRepository.php b/module/Core/src/Tag/Repository/TagRepository.php index d74da44ae..ce8b1f765 100644 --- a/module/Core/src/Tag/Repository/TagRepository.php +++ b/module/Core/src/Tag/Repository/TagRepository.php @@ -18,7 +18,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; use function array_map; -use function Functional\each; +use function array_walk; use function Shlinkio\Shlink\Core\camelCaseToSnakeCase; use const PHP_INT_MAX; @@ -95,7 +95,8 @@ public function findTagsWithInfo(?TagsListFiltering $filtering = null): array $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. diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php index 0002d3b13..e722bf253 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php @@ -27,9 +27,8 @@ 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 @@ public function expectedChannelsAreNotifiedBasedOnTheVisitType(Visit $visit, arr { $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 @@ public static function provideLegacyPayloads(): iterable 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 @@ function (MockObject & PublishingHelperInterface $helper) use ($visit): void { 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; diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php index 5b1740534..2cdbf6544 100644 --- a/module/Core/test/Importer/ImportedLinksProcessorTest.php +++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php @@ -32,8 +32,8 @@ use Symfony\Component\Console\Style\StyleInterface; use function count; -use function Functional\some; use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\some; use function sprintf; use function str_contains; diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index c1d66e619..ba6fab586 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -20,8 +20,8 @@ use Shlinkio\Shlink\Importer\Sources\ImportSource; use function array_map; -use function Functional\every; use function range; +use function Shlinkio\Shlink\Core\every; use function strlen; use function strtolower; diff --git a/module/Core/test/ShortUrl/Middleware/TrimTrailingSlashMiddlewareTest.php b/module/Core/test/ShortUrl/Middleware/TrimTrailingSlashMiddlewareTest.php index b43eed91d..b05ab7d9e 100644 --- a/module/Core/test/ShortUrl/Middleware/TrimTrailingSlashMiddlewareTest.php +++ b/module/Core/test/ShortUrl/Middleware/TrimTrailingSlashMiddlewareTest.php @@ -16,9 +16,6 @@ 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 @@ public function returnsExpectedResponse( 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(), ); diff --git a/module/Rest/src/ConfigProvider.php b/module/Rest/src/ConfigProvider.php index 7c57d8b19..067c69524 100644 --- a/module/Rest/src/ConfigProvider.php +++ b/module/Rest/src/ConfigProvider.php @@ -4,8 +4,9 @@ namespace Shlinkio\Shlink\Rest; +use function array_filter; use function array_map; -use function Functional\first; +use function reset; use function Shlinkio\Shlink\Config\loadConfigFromGlob; use function sprintf; @@ -34,8 +35,9 @@ public static function applyRoutesPrefix(array $routes): array 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; } diff --git a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php index 685d37958..8cfb918cb 100644 --- a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php +++ b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php @@ -15,8 +15,8 @@ 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 @@ private function remapTypeInArray(array $wrappedArray): array 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', From 1854cc2f19fadbcf6a1169d736e4f63176ad0ffc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Nov 2023 18:09:15 +0100 Subject: [PATCH 4/4] Remove last references to functional-php --- CHANGELOG.md | 17 +++++ composer.json | 2 +- config/autoload/entity-manager.global.php | 2 +- config/test/test_config.global.php | 2 +- data/migrations/Version20200105165647.php | 9 +-- .../src/Command/Db/CreateDatabaseCommand.php | 15 ++-- .../ShortUrl/CreateShortUrlCommand.php | 2 +- .../Visit/AbstractVisitsListCommand.php | 14 +--- module/Core/functions/array-utils.php | 74 +++++++++++++++++++ module/Core/functions/functions.php | 49 ------------ module/Core/src/Action/Model/QrCodeParams.php | 2 +- .../src/Config/NotFoundRedirectResolver.php | 40 ++++++---- .../src/ShortUrl/Model/DeviceLongUrlPair.php | 2 +- .../src/ShortUrl/Model/OrderableField.php | 2 +- .../Validation/DeviceLongUrlsValidator.php | 4 +- module/Core/src/Util/RedirectStatus.php | 2 +- module/Core/src/Visit/RequestTracker.php | 11 +-- .../NotifyVisitToWebHooksTest.php | 2 +- .../Importer/ImportedLinksProcessorTest.php | 4 +- .../test/ShortUrl/Entity/ShortUrlTest.php | 2 +- .../Middleware/AuthenticationMiddleware.php | 2 +- .../src/Middleware/BodyParserMiddleware.php | 2 +- 22 files changed, 147 insertions(+), 114 deletions(-) create mode 100644 module/Core/functions/array-utils.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b0d0d1ae..3b483eced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] +### Added +* *Nothing* + +### Changed +* Remove dependency on functional-php library + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [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. diff --git a/composer.json b/composer.json index e295539ef..6d013e091 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,6 @@ "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", @@ -91,6 +90,7 @@ }, "files": [ "config/constants.php", + "module/Core/functions/array-utils.php", "module/Core/functions/functions.php" ] }, diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php index 440956560..849c91af0 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -5,7 +5,7 @@ use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Shlinkio\Shlink\Core\Config\EnvVars; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; return (static function (): array { $driver = EnvVars::DB_DRIVER->loadFromEnv(); diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 75937bece..8ae64d7ae 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -30,7 +30,7 @@ use function file_exists; use function Laminas\Stratigility\middleware; use function Shlinkio\Shlink\Config\env; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function sprintf; use function sys_get_temp_dir; diff --git a/data/migrations/Version20200105165647.php b/data/migrations/Version20200105165647.php index bb4970219..26f8cc0a7 100644 --- a/data/migrations/Version20200105165647.php +++ b/data/migrations/Version20200105165647.php @@ -11,7 +11,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; -use function Shlinkio\Shlink\Core\some; +use function Shlinkio\Shlink\Core\ArrayUtils\some; final class Version20200105165647 extends AbstractMigration { @@ -23,12 +23,11 @@ final class Version20200105165647 extends AbstractMigration public function preUp(Schema $schema): void { $visitLocations = $schema->getTable('visit_locations'); - $this->skipIf(some( - self::COLUMNS, - fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName), + $this->skipIf(some( + self::COLUMNS, + fn (string $v, string|int $newColName) => $visitLocations->hasColumn((string) $newColName), ), 'New columns already exist'); - foreach (self::COLUMNS as $columnName) { $qb = $this->connection->createQueryBuilder(); $qb->update('visit_locations') diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php index b9bb8f104..53b854d1d 100644 --- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php +++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php @@ -17,7 +17,8 @@ use Throwable; use function array_map; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\some; class CreateDatabaseCommand extends AbstractDatabaseCommand { @@ -71,15 +72,9 @@ private function databaseTablesExist(): bool $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); $shlinkTables = array_map(static fn (ClassMetadata $metadata) => $metadata->getTableName(), $allMetadata); - foreach ($shlinkTables as $shlinkTable) { - // 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. - if (contains($shlinkTable, $existingTables)) { - return true; - } - } - - return false; + // 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($shlinkTable, $existingTables)); } private function ensureDatabaseExistsAndGetTables(): array diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index 3277f7634..64418aa62 100644 --- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -22,7 +22,7 @@ use function array_map; use function array_unique; use function explode; -use function Shlinkio\SHlink\Core\flatten; +use function Shlinkio\Shlink\Core\ArrayUtils\flatten; use function sprintf; class CreateShortUrlCommand extends Command diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php index 8766ecc55..a15eb5e72 100644 --- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -16,14 +16,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use function array_filter; use function array_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; -use function Shlinkio\Shlink\Core\contains; - -use const ARRAY_FILTER_USE_KEY; abstract class AbstractVisitsListCommand extends Command { @@ -64,14 +61,7 @@ private function resolveRowsAndHeaders(Paginator $paginator): array ]; // Filter out unknown keys - return array_filter( - $rowData, - static fn (string $key) => contains( - $key, - ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys], - ), - ARRAY_FILTER_USE_KEY, - ); + return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); }, [...$paginator->getCurrentPageResults()]); $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); diff --git a/module/Core/functions/array-utils.php b/module/Core/functions/array-utils.php new file mode 100644 index 000000000..5fb636e69 --- /dev/null +++ b/module/Core/functions/array-utils.php @@ -0,0 +1,74 @@ + [...$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, + ); +} diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index f13fc670f..d07bc9e21 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -20,7 +20,6 @@ use function array_map; use function array_reduce; use function date_default_timezone_get; -use function in_array; use function is_array; use function print_r; use function Shlinkio\Shlink\Common\buildDateRange; @@ -183,51 +182,3 @@ function enumValues(string $enum): array $cache[$enum] = array_map(static fn (BackedEnum $type) => (string) $type->value, $enum::cases()) ); } - -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, string|number $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; -} diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 51162d5f3..05181f20b 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -18,7 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Options\QrCodeOptions; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function strtolower; use function trim; diff --git a/module/Core/src/Config/NotFoundRedirectResolver.php b/module/Core/src/Config/NotFoundRedirectResolver.php index 540956ee7..ce5401d2e 100644 --- a/module/Core/src/Config/NotFoundRedirectResolver.php +++ b/module/Core/src/Config/NotFoundRedirectResolver.php @@ -12,7 +12,6 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType; use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface; -use function Functional\compose; use function str_replace; use function urlencode; @@ -51,9 +50,6 @@ public function resolveRedirectResponse( private function resolvePlaceholders(UriInterface $currentUri, string $redirectUrl): string { - $domain = $currentUri->getAuthority(); - $path = $currentUri->getPath(); - try { $redirectUri = Uri::createFromString($redirectUrl); } catch (SyntaxError $e) { @@ -64,18 +60,32 @@ private function resolvePlaceholders(UriInterface $currentUri, string $redirectU 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), - ); - $replacePlaceholdersInPath = compose( - $replacePlaceholders(static fn (mixed $v) => $v), - static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), + $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, ); - $replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...)); return $redirectUri ->withPath($replacePlaceholdersInPath($redirectUri->getPath())) diff --git a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php index a83ec01d9..a48c666bd 100644 --- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php +++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php @@ -24,7 +24,7 @@ public static function fromRawTypeAndLongUrl(string $type, string $longUrl): sel * * 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 $map + * @param array $map * @return array{array, DeviceType[]} */ public static function fromMapToChangeSet(array $map): array diff --git a/module/Core/src/ShortUrl/Model/OrderableField.php b/module/Core/src/ShortUrl/Model/OrderableField.php index 1b61a1557..685f6f12a 100644 --- a/module/Core/src/ShortUrl/Model/OrderableField.php +++ b/module/Core/src/ShortUrl/Model/OrderableField.php @@ -2,7 +2,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; enum OrderableField: string { diff --git a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php index 0c3b19c24..82119e4e9 100644 --- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php +++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php @@ -11,9 +11,9 @@ use function array_keys; use function array_values; use function is_array; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\every; use function Shlinkio\Shlink\Core\enumValues; -use function Shlinkio\Shlink\Core\every; class DeviceLongUrlsValidator extends AbstractValidator { diff --git a/module/Core/src/Util/RedirectStatus.php b/module/Core/src/Util/RedirectStatus.php index 313dc4325..f561e2125 100644 --- a/module/Core/src/Util/RedirectStatus.php +++ b/module/Core/src/Util/RedirectStatus.php @@ -2,7 +2,7 @@ namespace Shlinkio\Shlink\Core\Util; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; enum RedirectStatus: int { diff --git a/module/Core/src/Visit/RequestTracker.php b/module/Core/src/Visit/RequestTracker.php index e86471658..1a6b04f99 100644 --- a/module/Core/src/Visit/RequestTracker.php +++ b/module/Core/src/Visit/RequestTracker.php @@ -20,6 +20,7 @@ use function array_map; use function explode; use function implode; +use function Shlinkio\Shlink\Core\ArrayUtils\some; use function str_contains; class RequestTracker implements RequestTrackerInterface, RequestMethodInterface @@ -85,17 +86,13 @@ private function shouldDisableTrackingFromAddress(?string $remoteAddr): bool $remoteAddrParts = explode('.', $remoteAddr); $disableTrackingFrom = $this->trackingOptions->disableTrackingFrom; - foreach ($disableTrackingFrom as $value) { + return some($disableTrackingFrom, function (string $value) use ($ip, $remoteAddrParts): bool { $range = str_contains($value, '*') ? $this->parseValueWithWildcards($value, $remoteAddrParts) : Factory::parseRangeString($value); - if ($range !== null && $ip->matches($range)) { - return true; - } - } - - return false; + return $range !== null && $ip->matches($range); + }); } private function parseValueWithWildcards(string $value, array $remoteAddrParts): ?RangeInterface diff --git a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php index c4ca402a3..8b9c10aca 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php @@ -28,7 +28,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor; use function count; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; class NotifyVisitToWebHooksTest extends TestCase { diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php index 2cdbf6544..921273c1e 100644 --- a/module/Core/test/Importer/ImportedLinksProcessorTest.php +++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php @@ -32,8 +32,8 @@ use Symfony\Component\Console\Style\StyleInterface; use function count; -use function Shlinkio\Shlink\Core\contains; -use function Shlinkio\Shlink\Core\some; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\some; use function sprintf; use function str_contains; diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index ba6fab586..0a898399c 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -21,7 +21,7 @@ use function array_map; use function range; -use function Shlinkio\Shlink\Core\every; +use function Shlinkio\Shlink\Core\ArrayUtils\every; use function strlen; use function strtolower; diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php index 85ec61b7c..cf73ba10c 100644 --- a/module/Rest/src/Middleware/AuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php @@ -17,7 +17,7 @@ use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface { diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php index b0548f975..cdab82995 100644 --- a/module/Rest/src/Middleware/BodyParserMiddleware.php +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -12,7 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Exception\MalformedBodyException; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function Shlinkio\Shlink\Json\json_decode; class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface