From f50263d2d992f127fe796b6b3b21ebb0e13928a1 Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandrocelaya@gmail.com>
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 e44ca82b..f33693ad 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 4fd4b005..b55dcd7d 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\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 
 use function array_filter;
-use function Functional\map;
+use function array_map;
 use function implode;
 use function sprintf;
 
@@ -49,7 +49,7 @@ class ListKeysCommand extends Command
     {
         $enabledOnly = $input->getOption('enabled-only');
 
-        $rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
+        $rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
             $expiration = $apiKey->getExpirationDate();
             $messagePattern = $this->determineMessagePattern($apiKey);
 
@@ -64,7 +64,7 @@ class ListKeysCommand extends Command
             ));
 
             return $rowData;
-        });
+        }, $this->apiKeyService->listKeys($enabledOnly));
 
         ShlinkTable::withRowSeparators($output)->render(array_filter([
             'Key',
diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
index 129db1e0..c70e2f76 100644
--- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php
+++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
@@ -16,8 +16,8 @@ use Symfony\Component\Lock\LockFactory;
 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 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
     {
         $existingTables = $this->ensureDatabaseExistsAndGetTables();
         $allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
-        $shlinkTables = map($allMetadata, static fn (ClassMetadata $metadata) => $metadata->getTableName());
+        $shlinkTables = array_map(static fn (ClassMetadata $metadata) => $metadata->getTableName(), $allMetadata);
 
         // If at least one of the shlink tables exist, we will consider the database exists somehow.
         // Any other inconsistency will be taken care of by the migrations.
diff --git a/module/CLI/src/Command/Domain/ListDomainsCommand.php b/module/CLI/src/Command/Domain/ListDomainsCommand.php
index 11a0f5b9..50107292 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\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 
-use function Functional\map;
+use function array_map;
 
 class ListDomainsCommand extends Command
 {
     public const NAME = 'domain:list';
 
-    public function __construct(private DomainServiceInterface $domainService)
+    public function __construct(private readonly DomainServiceInterface $domainService)
     {
         parent::__construct();
     }
@@ -47,7 +47,7 @@ class ListDomainsCommand extends Command
 
         $table->render(
             $showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
-            map($domains, function (DomainItem $domain) use ($showRedirects) {
+            array_map(function (DomainItem $domain) use ($showRedirects) {
                 $commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
 
                 return $showRedirects
@@ -56,7 +56,7 @@ class ListDomainsCommand extends Command
                         $this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
                       ]
                     : $commonValues;
-            }),
+            }, $domains),
         );
 
         return ExitCode::EXIT_SUCCESS;
diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php
index 14ea1851..c9497daf 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\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
 use function array_keys;
+use function array_map;
 use function array_pad;
 use function explode;
-use function Functional\map;
 use function implode;
 use function sprintf;
 
@@ -184,10 +184,10 @@ class ListShortUrlsCommand extends Command
     ): Paginator {
         $shortUrls = $this->shortUrlService->listShortUrls($params);
 
-        $rows = map($shortUrls, function (ShortUrl $shortUrl) use ($columnsMap) {
+        $rows = array_map(function (ShortUrl $shortUrl) use ($columnsMap) {
             $rawShortUrl = $this->transformer->transform($shortUrl);
-            return map($columnsMap, fn (callable $call) => $call($rawShortUrl, $shortUrl));
-        });
+            return array_map(fn (callable $call) => $call($rawShortUrl, $shortUrl), $columnsMap);
+        }, [...$shortUrls]);
 
         ShlinkTable::default($output)->render(
             array_keys($columnsMap),
diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php
index 41ca9b60..d56e4101 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\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
-use function Functional\map;
+use function array_map;
 
 class ListTagsCommand extends Command
 {
     public const NAME = 'tag:list';
 
-    public function __construct(private TagServiceInterface $tagService)
+    public function __construct(private readonly TagServiceInterface $tagService)
     {
         parent::__construct();
     }
@@ -44,9 +44,9 @@ class ListTagsCommand extends Command
             return [['No tags found', '-', '-']];
         }
 
-        return map(
-            $tags,
+        return array_map(
             static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsSummary->total],
+            [...$tags],
         );
     }
 }
diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
index ba518656..a247380e 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\Command\Command;
 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 @@ abstract class AbstractVisitsListCommand extends Command
     private function resolveRowsAndHeaders(Paginator $paginator): array
     {
         $extraKeys = [];
-        $rows = map($paginator->getCurrentPageResults(), function (Visit $visit) use (&$extraKeys) {
+        $rows = array_map(function (Visit $visit) use (&$extraKeys) {
             $extraFields = $this->mapExtraFields($visit);
             $extraKeys = array_keys($extraFields);
 
@@ -60,9 +63,18 @@ abstract class AbstractVisitsListCommand extends Command
                 ...$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 7aecda6d..cbd4f0fa 100644
--- a/module/CLI/test/ApiKey/RoleResolverTest.php
+++ b/module/CLI/test/ApiKey/RoleResolverTest.php
@@ -16,8 +16,6 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
 use Shlinkio\Shlink\Rest\ApiKey\Role;
 use Symfony\Component\Console\Input\InputInterface;
 
-use function Functional\map;
-
 class RoleResolverTest extends TestCase
 {
     private RoleResolver $resolver;
@@ -49,10 +47,13 @@ class RoleResolverTest extends TestCase
     {
         $domain = self::domainWithId(Domain::withAuthority('example.com'));
         $buildInput = static fn (array $definition) => function (TestCase $test) use ($definition): InputInterface {
+            $returnMap = [];
+            foreach ($definition as $param => $returnValue) {
+                $returnMap[] = [$param, $returnValue];
+            }
+
             $input = $test->createStub(InputInterface::class);
-            $input->method('getOption')->willReturnMap(
-                map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
-            );
+            $input->method('getOption')->willReturnMap($returnMap);
 
             return $input;
         };
diff --git a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php
index 9d32ca79..0f911db8 100644
--- a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php
+++ b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php
@@ -21,7 +21,7 @@ use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
 use Symfony\Component\Lock;
 use Throwable;
 
-use function Functional\map;
+use function array_map;
 use function range;
 
 class GeolocationDbUpdaterTest extends TestCase
@@ -128,7 +128,7 @@ class GeolocationDbUpdaterTest extends TestCase
             return [$days % 2 === 0 ? $timestamp : (string) $timestamp];
         };
 
-        return map(range(0, 34), $generateParamsWithTimestamp);
+        return array_map($generateParamsWithTimestamp, range(0, 34));
     }
 
     #[Test]
diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php
index b6acbb35..32d357e3 100644
--- a/module/Core/functions/functions.php
+++ b/module/Core/functions/functions.php
@@ -17,8 +17,8 @@ use PUGX\Shortid\Factory as ShortIdFactory;
 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 619e6056..616759f1 100644
--- a/module/Core/src/Config/PostProcessor/BasePathPrefixer.php
+++ b/module/Core/src/Config/PostProcessor/BasePathPrefixer.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\Core\Config\PostProcessor;
 
-use function Functional\map;
+use function array_map;
 
 class BasePathPrefixer
 {
@@ -23,13 +23,13 @@ class BasePathPrefixer
 
     private function prefixPathsWithBasePath(string $configKey, array $config, string $basePath): array
     {
-        return map($config[$configKey] ?? [], function (array $element) use ($basePath) {
+        return array_map(function (array $element) use ($basePath) {
             if (! isset($element['path'])) {
                 return $element;
             }
 
             $element['path'] = $basePath . $element['path'];
             return $element;
-        });
+        }, $config[$configKey] ?? []);
     }
 }
diff --git a/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php b/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php
index 33945063..585f78b6 100644
--- a/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php
+++ b/module/Core/src/Config/PostProcessor/MultiSegmentSlugProcessor.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\Core\Config\PostProcessor;
 
-use function Functional\map;
+use function array_map;
 use function str_replace;
 
 class MultiSegmentSlugProcessor
@@ -19,11 +19,11 @@ class MultiSegmentSlugProcessor
             return $config;
         }
 
-        $config['routes'] = map($config['routes'] ?? [], static function (array $route): array {
+        $config['routes'] = array_map(static function (array $route): array {
             ['path' => $path] = $route;
             $route['path'] = str_replace(self::SINGLE_SEGMENT_PATTERN, self::MULTI_SEGMENT_PATTERN, $path);
             return $route;
-        });
+        }, $config['routes'] ?? []);
 
         return $config;
     }
diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php
index 703f77fd..9aa4e3d0 100644
--- a/module/Core/src/Domain/DomainService.php
+++ b/module/Core/src/Domain/DomainService.php
@@ -14,9 +14,9 @@ use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
 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 @@ class DomainService implements DomainServiceInterface
     public function listDomains(?ApiKey $apiKey = null): array
     {
         [$default, $domains] = $this->defaultDomainAndRest($apiKey);
-        $mappedDomains = map($domains, fn (Domain $domain) => DomainItem::forNonDefaultDomain($domain));
+        $mappedDomains = array_map(fn (Domain $domain) => DomainItem::forNonDefaultDomain($domain), $domains);
 
         if ($apiKey?->hasRole(Role::DOMAIN_SPECIFIC)) {
             return $mappedDomains;
diff --git a/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php b/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php
index 317821b1..028c3c13 100644
--- a/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php
+++ b/module/Core/src/EventDispatcher/NotifyVisitToWebHooks.php
@@ -19,18 +19,18 @@ use Shlinkio\Shlink\Core\Options\WebhookOptions;
 use Shlinkio\Shlink\Core\Visit\Entity\Visit;
 use Throwable;
 
-use function Functional\map;
+use function array_map;
 
 /** @deprecated */
 class NotifyVisitToWebHooks
 {
     public function __construct(
-        private ClientInterface $httpClient,
-        private EntityManagerInterface $em,
-        private LoggerInterface $logger,
-        private WebhookOptions $webhookOptions,
-        private DataTransformerInterface $transformer,
-        private AppOptions $appOptions,
+        private readonly ClientInterface $httpClient,
+        private readonly EntityManagerInterface $em,
+        private readonly LoggerInterface $logger,
+        private readonly WebhookOptions $webhookOptions,
+        private readonly DataTransformerInterface $transformer,
+        private readonly AppOptions $appOptions,
     ) {
     }
 
@@ -82,11 +82,11 @@ class NotifyVisitToWebHooks
      */
     private function performRequests(array $requestOptions, string $visitId): array
     {
-        return map(
-            $this->webhookOptions->webhooks(),
+        return array_map(
             fn (string $webhook): PromiseInterface => $this->httpClient
                 ->requestAsync(RequestMethodInterface::METHOD_POST, $webhook, $requestOptions)
                 ->otherwise(fn (Throwable $e) => $this->logWebhookFailure($webhook, $visitId, $e)),
+            $this->webhookOptions->webhooks(),
         );
     }
 
diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php
index 8fbec5ed..e53e9afa 100644
--- a/module/Core/src/ShortUrl/Entity/ShortUrl.php
+++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php
@@ -27,8 +27,8 @@ use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
 use Shlinkio\Shlink\Rest\Entity\ApiKey;
 
 use function array_fill_keys;
+use function array_map;
 use function count;
-use function Functional\map;
 use function Shlinkio\Shlink\Core\enumValues;
 use function Shlinkio\Shlink\Core\generateRandomShortCode;
 use function Shlinkio\Shlink\Core\normalizeDate;
@@ -90,9 +90,9 @@ class ShortUrl extends AbstractEntity
         $instance->longUrl = $creation->getLongUrl();
         $instance->dateCreated = Chronos::now();
         $instance->visits = new ArrayCollection();
-        $instance->deviceLongUrls = new ArrayCollection(map(
-            $creation->deviceLongUrls,
+        $instance->deviceLongUrls = new ArrayCollection(array_map(
             fn (DeviceLongUrlPair $pair) => DeviceLongUrl::fromShortUrlAndPair($instance, $pair),
+            $creation->deviceLongUrls,
         ));
         $instance->tags = $relationResolver->resolveTags($creation->tags);
         $instance->validSince = $creation->validSince;
diff --git a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
index d017c7e5..c7b1efc0 100644
--- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
+++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
@@ -6,9 +6,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
 
 use Shlinkio\Shlink\Core\Model\DeviceType;
 
-use function array_values;
 use function Functional\group;
-use function Functional\map;
 use function trim;
 
 final class DeviceLongUrlPair
@@ -32,15 +30,23 @@ final class DeviceLongUrlPair
      */
     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 17669f32..6c49ab5f 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\Lock;
 use Symfony\Component\Lock\LockFactory;
 use Symfony\Component\Lock\Store\InMemoryStore;
 
-use function Functional\invoke;
-use function Functional\map;
-use function Functional\unique;
+use function array_map;
+use function array_unique;
 
 class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInterface
 {
@@ -74,10 +73,10 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
             return new Collections\ArrayCollection();
         }
 
-        $tags = unique($tags);
+        $tags = array_unique($tags);
         $repo = $this->em->getRepository(Tag::class);
 
-        return new Collections\ArrayCollection(map($tags, function (string $tagName) use ($repo): Tag {
+        return new Collections\ArrayCollection(array_map(function (string $tagName) use ($repo): Tag {
             $this->lock($this->tagLocks, 'tag_' . $tagName);
 
             $existingTag = $repo->findOneBy(['name' => $tagName]);
@@ -91,7 +90,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
             $this->em->persist($tag);
 
             return $tag;
-        }));
+        }, $tags));
     }
 
     private function memoizeNewTag(string $tagName): Tag
@@ -110,6 +109,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
         $lock->acquire(true);
     }
 
+    /**
     /**
      * @param array<string, Lock> $locks
      */
@@ -126,9 +126,15 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
         $this->memoizedNewTags = [];
 
         // Release all locks
-        invoke($this->tagLocks, 'release');
-        invoke($this->domainLocks, 'release');
-        $this->tagLocks = [];
-        $this->domainLocks = [];
+        $this->releaseLocks($this->tagLocks);
+        $this->releaseLocks($this->domainLocks);
+    }
+
+    private function releaseLocks(array &$locks): void
+    {
+        foreach ($locks as $tagLock) {
+            $tagLock->release();
+        }
+        $locks = [];
     }
 }
diff --git a/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php b/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php
index 609a300c..c1a9d0ab 100644
--- a/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php
+++ b/module/Core/src/ShortUrl/Resolver/SimpleShortUrlRelationResolver.php
@@ -8,7 +8,7 @@ use Doctrine\Common\Collections;
 use Shlinkio\Shlink\Core\Domain\Entity\Domain;
 use Shlinkio\Shlink\Core\Tag\Entity\Tag;
 
-use function Functional\map;
+use function array_map;
 
 class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterface
 {
@@ -23,6 +23,6 @@ class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterfac
      */
     public function resolveTags(array $tags): Collections\Collection
     {
-        return new Collections\ArrayCollection(map($tags, fn (string $tag) => new Tag($tag)));
+        return new Collections\ArrayCollection(array_map(fn (string $tag) => new Tag($tag), $tags));
     }
 }
diff --git a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php
index 9de5c408..a6641998 100644
--- a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php
+++ b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php
@@ -7,10 +7,10 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Transformer;
 use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
 use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
 use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
+use Shlinkio\Shlink\Core\Tag\Entity\Tag;
 use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
 
-use function Functional\invoke;
-use function Functional\invoke_if;
+use function array_map;
 
 class ShortUrlDataTransformer implements DataTransformerInterface
 {
@@ -29,7 +29,7 @@ class ShortUrlDataTransformer implements DataTransformerInterface
             'longUrl' => $shortUrl->getLongUrl(),
             'deviceLongUrls' => $shortUrl->deviceLongUrls(),
             'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
-            'tags' => invoke($shortUrl->getTags(), '__toString'),
+            'tags' => array_map(static fn (Tag $tag) => $tag->__toString(), $shortUrl->getTags()->toArray()),
             'meta' => $this->buildMeta($shortUrl),
             'domain' => $shortUrl->getDomain(),
             'title' => $shortUrl->title(),
@@ -52,8 +52,8 @@ class ShortUrlDataTransformer implements DataTransformerInterface
         $maxVisits = $shortUrl->getMaxVisits();
 
         return [
-            'validSince' => invoke_if($validSince, 'toAtomString'),
-            'validUntil' => invoke_if($validUntil, 'toAtomString'),
+            'validSince' => $validSince?->toAtomString(),
+            'validUntil' => $validUntil?->toAtomString(),
             'maxVisits' => $maxVisits,
         ];
     }
diff --git a/module/Core/src/Tag/Repository/TagRepository.php b/module/Core/src/Tag/Repository/TagRepository.php
index 278dbe8b..d74da44a 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\Role;
 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 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
         $rsm->addScalarResult('non_bot_visits', 'nonBotVisits');
         $rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
 
-        return map(
-            $this->getEntityManager()->createNativeQuery($mainQb->getSQL(), $rsm)->getResult(),
+        return array_map(
             TagInfo::fromRawData(...),
+            $this->getEntityManager()->createNativeQuery($mainQb->getSQL(), $rsm)->getResult(),
         );
     }
 
diff --git a/module/Core/src/Visit/RequestTracker.php b/module/Core/src/Visit/RequestTracker.php
index cb43e10d..e8647165 100644
--- a/module/Core/src/Visit/RequestTracker.php
+++ b/module/Core/src/Visit/RequestTracker.php
@@ -16,9 +16,9 @@ use Shlinkio\Shlink\Core\Options\TrackingOptions;
 use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
 use Shlinkio\Shlink\Core\Visit\Model\Visitor;
 
+use function array_keys;
+use function array_map;
 use function explode;
-use function Functional\map;
-use function Functional\some;
 use function implode;
 use function str_contains;
 
@@ -85,22 +85,30 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
         $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 46c08d25..b359e35d 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\Entity\Visit;
 use Shlinkio\Shlink\Core\Visit\Model\Visitor;
 use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
 
+use function array_map;
 use function count;
-use function Functional\map;
 use function range;
 
 class ShortUrlListRepositoryTest extends DatabaseTestCase
@@ -60,22 +60,22 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
         $this->getEntityManager()->persist($foo);
 
         $bar = ShortUrl::withLongUrl('https://bar');
-        $visits = map(range(0, 5), function () use ($bar) {
+        $visits = array_map(function () use ($bar) {
             $visit = Visit::forValidShortUrl($bar, Visitor::botInstance());
             $this->getEntityManager()->persist($visit);
 
             return $visit;
-        });
+        }, range(0, 5));
         $bar->setVisits(new ArrayCollection($visits));
         $this->getEntityManager()->persist($bar);
 
         $foo2 = ShortUrl::withLongUrl('https://foo_2');
-        $visits2 = map(range(0, 3), function () use ($foo2) {
+        $visits2 = array_map(function () use ($foo2) {
             $visit = Visit::forValidShortUrl($foo2, Visitor::emptyInstance());
             $this->getEntityManager()->persist($visit);
 
             return $visit;
-        });
+        }, range(0, 3));
         $foo2->setVisits(new ArrayCollection($visits2));
         $ref = new ReflectionObject($foo2);
         $dateProp = $ref->getProperty('dateCreated');
diff --git a/module/Core/test-db/Tag/Paginator/Adapter/TagsPaginatorAdapterTest.php b/module/Core/test-db/Tag/Paginator/Adapter/TagsPaginatorAdapterTest.php
index 0dd83341..f88a8e7f 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\Paginator\Adapter\TagsPaginatorAdapter;
 use Shlinkio\Shlink\Core\Tag\Repository\TagRepository;
 use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
 
-use function Functional\map;
+use function array_map;
 
 class TagsPaginatorAdapterTest extends DatabaseTestCase
 {
@@ -47,7 +47,7 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
             'orderBy' => $orderBy,
         ]), null);
 
-        $tagNames = map($adapter->getSlice($offset, $length), static fn (Tag $tag) => $tag->__toString());
+        $tagNames = array_map(static fn (Tag $tag) => $tag->__toString(), [...$adapter->getSlice($offset, $length)]);
 
         self::assertEquals($expectedTags, $tagNames);
         self::assertEquals($expectedTotalCount, $adapter->getNbResults());
diff --git a/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php
index 79c80a24..c5aadf1f 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\Core\Visit\Repository\VisitLocationRepository;
 use Shlinkio\Shlink\IpGeolocation\Model\Location;
 use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
 
-use function Functional\map;
+use function array_map;
 use function range;
 
 class VisitLocationRepositoryTest extends DatabaseTestCase
@@ -57,6 +57,6 @@ class VisitLocationRepositoryTest extends DatabaseTestCase
 
     public static function provideBlockSize(): iterable
     {
-        return map(range(1, 10), fn (int $value) => [$value]);
+        return array_map(static fn (int $value) => [$value], range(1, 10));
     }
 }
diff --git a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php
index 6ba20ec8..dc604521 100644
--- a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php
+++ b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php
@@ -16,7 +16,7 @@ use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
 use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
 use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb;
 
-use function Functional\map;
+use function array_map;
 
 class UpdateGeoLiteDbTest extends TestCase
 {
@@ -124,9 +124,9 @@ class UpdateGeoLiteDbTest extends TestCase
 
     public static function provideGeolocationResults(): iterable
     {
-        return map(GeolocationResult::cases(), static fn (GeolocationResult $value) => [
+        return array_map(static fn (GeolocationResult $value) => [
             $value,
             $value === GeolocationResult::DB_CREATED ? 1 : 0,
-        ]);
+        ], GeolocationResult::cases());
     }
 }
diff --git a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php
index 8d82c11e..c1b2bcec 100644
--- a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php
+++ b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php
@@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase;
 use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
 use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
 
-use function Functional\map;
+use function array_map;
 use function range;
 use function Shlinkio\Shlink\Core\generateRandomShortCode;
 use function sprintf;
@@ -42,13 +42,13 @@ class DeleteShortUrlExceptionTest extends TestCase
 
     public static function provideThresholds(): array
     {
-        return map(range(5, 50, 5), function (int $number) {
+        return array_map(function (int $number) {
             return [$number, $shortCode = generateRandomShortCode(6), sprintf(
                 'Impossible to delete short URL with short code "%s", since it has more than "%s" visits.',
                 $shortCode,
                 $number,
             )];
-        });
+        }, range(5, 50, 5));
     }
 
     #[Test]
diff --git a/module/Core/test/Functions/FunctionsTest.php b/module/Core/test/Functions/FunctionsTest.php
index 3f6026a0..715685af 100644
--- a/module/Core/test/Functions/FunctionsTest.php
+++ b/module/Core/test/Functions/FunctionsTest.php
@@ -13,7 +13,7 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
 use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField;
 use Shlinkio\Shlink\Core\Visit\Model\VisitType;
 
-use function Functional\map;
+use function array_map;
 use function Shlinkio\Shlink\Core\enumValues;
 
 class FunctionsTest extends TestCase
@@ -29,18 +29,21 @@ class FunctionsTest extends TestCase
 
     public static function provideEnums(): iterable
     {
-        yield EnvVars::class => [EnvVars::class, map(EnvVars::cases(), static fn (EnvVars $envVar) => $envVar->value)];
+        yield EnvVars::class => [
+            EnvVars::class,
+            array_map(static fn (EnvVars $envVar) => $envVar->value, EnvVars::cases()),
+        ];
         yield VisitType::class => [
             VisitType::class,
-            map(VisitType::cases(), static fn (VisitType $envVar) => $envVar->value),
+            array_map(static fn (VisitType $envVar) => $envVar->value, VisitType::cases()),
         ];
         yield DeviceType::class => [
             DeviceType::class,
-            map(DeviceType::cases(), static fn (DeviceType $envVar) => $envVar->value),
+            array_map(static fn (DeviceType $envVar) => $envVar->value, DeviceType::cases()),
         ];
         yield OrderableField::class => [
             OrderableField::class,
-            map(OrderableField::cases(), static fn (OrderableField $envVar) => $envVar->value),
+            array_map(static fn (OrderableField $envVar) => $envVar->value, OrderableField::cases()),
         ];
     }
 }
diff --git a/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php
index 65351a93..3ac9897c 100644
--- a/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php
+++ b/module/Core/test/ShortUrl/DeleteShortUrlServiceTest.php
@@ -18,7 +18,7 @@ use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
 use Shlinkio\Shlink\Core\Visit\Entity\Visit;
 use Shlinkio\Shlink\Core\Visit\Model\Visitor;
 
-use function Functional\map;
+use function array_map;
 use function range;
 use function sprintf;
 
@@ -31,7 +31,7 @@ class DeleteShortUrlServiceTest extends TestCase
     protected function setUp(): void
     {
         $shortUrl = ShortUrl::createFake()->setVisits(new ArrayCollection(
-            map(range(0, 10), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance())),
+            array_map(fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()), range(0, 10)),
         ));
         $this->shortCode = $shortUrl->getShortCode();
 
diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php
index bd83fd9a..c1d66e61 100644
--- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php
+++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php
@@ -19,8 +19,8 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
 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 @@ class ShortUrlTest extends TestCase
     public static function provideLengths(): iterable
     {
         yield [null, DEFAULT_SHORT_CODES_LENGTH];
-        yield from map(range(4, 10), fn (int $value) => [$value, $value]);
+        yield from array_map(fn (int $value) => [$value, $value], range(4, 10));
     }
 
     #[Test]
diff --git a/module/Core/test/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/ShortUrl/ShortUrlResolverTest.php
index 4057691b..a95426ba 100644
--- a/module/Core/test/ShortUrl/ShortUrlResolverTest.php
+++ b/module/Core/test/ShortUrl/ShortUrlResolverTest.php
@@ -25,7 +25,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor;
 use Shlinkio\Shlink\Rest\Entity\ApiKey;
 use ShlinkioTest\Shlink\Core\Util\ApiKeyDataProviders;
 
-use function Functional\map;
+use function array_map;
 use function range;
 
 class ShortUrlResolverTest extends TestCase
@@ -113,9 +113,9 @@ class ShortUrlResolverTest extends TestCase
             $shortUrl = ShortUrl::create(
                 ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://longUrl']),
             );
-            $shortUrl->setVisits(new ArrayCollection(map(
-                range(0, 4),
+            $shortUrl->setVisits(new ArrayCollection(array_map(
                 fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
+                range(0, 4),
             )));
 
             return $shortUrl;
@@ -132,9 +132,9 @@ class ShortUrlResolverTest extends TestCase
                 'validUntil' => $now->subMonths(1)->toAtomString(),
                 'longUrl' => 'https://longUrl',
             ]));
-            $shortUrl->setVisits(new ArrayCollection(map(
-                range(0, 4),
+            $shortUrl->setVisits(new ArrayCollection(array_map(
                 fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
+                range(0, 4),
             )));
 
             return $shortUrl;
diff --git a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php
index 27916063..349392db 100644
--- a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php
+++ b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php
@@ -84,4 +84,14 @@ class ShortUrlDataTransformerTest extends TestCase
             ],
         ];
     }
+
+    #[Test]
+    public function properTagsAreReturned(): void
+    {
+        ['tags' => $tags] = $this->transformer->transform(ShortUrl::create(ShortUrlCreation::fromRawData([
+            'longUrl' => 'https://longUrl',
+            'tags' => ['foo', 'bar', 'baz'],
+        ])));
+        self::assertEquals(['foo', 'bar', 'baz'], $tags);
+    }
 }
diff --git a/module/Core/test/Visit/Geolocation/VisitLocatorTest.php b/module/Core/test/Visit/Geolocation/VisitLocatorTest.php
index 70fc6243..1d3af228 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\Model\Visitor;
 use Shlinkio\Shlink\Core\Visit\Repository\VisitLocationRepositoryInterface;
 use Shlinkio\Shlink\IpGeolocation\Model\Location;
 
+use function array_map;
 use function count;
 use function floor;
-use function Functional\map;
 use function range;
 use function sprintf;
 
@@ -45,12 +45,12 @@ class VisitLocatorTest extends TestCase
         string $serviceMethodName,
         string $expectedRepoMethodName,
     ): void {
-        $unlocatedVisits = map(
-            range(1, 200),
+        $unlocatedVisits = array_map(
             fn (int $i) => Visit::forValidShortUrl(
                 ShortUrl::withLongUrl(sprintf('https://short_code_%s', $i)),
                 Visitor::emptyInstance(),
             ),
+            range(1, 200),
         );
 
         $this->repo->expects($this->once())->method($expectedRepoMethodName)->willReturn($unlocatedVisits);
diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php
index d43efc24..dd11fdef 100644
--- a/module/Core/test/Visit/VisitsStatsHelperTest.php
+++ b/module/Core/test/Visit/VisitsStatsHelperTest.php
@@ -33,8 +33,8 @@ use Shlinkio\Shlink\Core\Visit\VisitsStatsHelper;
 use Shlinkio\Shlink\Rest\Entity\ApiKey;
 use ShlinkioTest\Shlink\Core\Util\ApiKeyDataProviders;
 
+use function array_map;
 use function count;
-use function Functional\map;
 use function range;
 
 class VisitsStatsHelperTest extends TestCase
@@ -75,8 +75,8 @@ class VisitsStatsHelperTest extends TestCase
     public static function provideCounts(): iterable
     {
         return [
-            ...map(range(0, 50, 5), fn (int $value) => [$value, null]),
-            ...map(range(0, 18, 3), fn (int $value) => [$value, ApiKey::create()]),
+            ...array_map(fn (int $value) => [$value, null], range(0, 50, 5)),
+            ...array_map(fn (int $value) => [$value, ApiKey::create()], range(0, 18, 3)),
         ];
     }
 
@@ -90,7 +90,10 @@ class VisitsStatsHelperTest extends TestCase
         $repo = $this->createMock(ShortUrlRepositoryInterface::class);
         $repo->expects($this->once())->method('shortCodeIsInUse')->with($identifier, $spec)->willReturn(true);
 
-        $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
+        $list = array_map(
+            static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
+            range(0, 1),
+        );
         $repo2 = $this->createMock(VisitRepository::class);
         $repo2->method('findVisitsByShortCode')->with(
             $identifier,
@@ -147,7 +150,10 @@ class VisitsStatsHelperTest extends TestCase
         $repo = $this->createMock(TagRepository::class);
         $repo->expects($this->once())->method('tagExists')->with($tag, $apiKey)->willReturn(true);
 
-        $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
+        $list = array_map(
+            static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
+            range(0, 1),
+        );
         $repo2 = $this->createMock(VisitRepository::class);
         $repo2->method('findVisitsByTag')->with($tag, $this->isInstanceOf(VisitsListFiltering::class))->willReturn(
             $list,
@@ -185,7 +191,10 @@ class VisitsStatsHelperTest extends TestCase
         $repo = $this->createMock(DomainRepository::class);
         $repo->expects($this->once())->method('domainExists')->with($domain, $apiKey)->willReturn(true);
 
-        $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
+        $list = array_map(
+            static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
+            range(0, 1),
+        );
         $repo2 = $this->createMock(VisitRepository::class);
         $repo2->method('findVisitsByDomain')->with(
             $domain,
@@ -212,7 +221,10 @@ class VisitsStatsHelperTest extends TestCase
         $repo = $this->createMock(DomainRepository::class);
         $repo->expects($this->never())->method('domainExists');
 
-        $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
+        $list = array_map(
+            static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
+            range(0, 1),
+        );
         $repo2 = $this->createMock(VisitRepository::class);
         $repo2->method('findVisitsByDomain')->with(
             'DEFAULT',
@@ -236,7 +248,7 @@ class VisitsStatsHelperTest extends TestCase
     #[Test]
     public function orphanVisitsAreReturnedAsExpected(): void
     {
-        $list = map(range(0, 3), fn () => Visit::forBasePath(Visitor::emptyInstance()));
+        $list = array_map(static fn () => Visit::forBasePath(Visitor::emptyInstance()), range(0, 3));
         $repo = $this->createMock(VisitRepository::class);
         $repo->expects($this->once())->method('countOrphanVisits')->with(
             $this->isInstanceOf(VisitsCountFiltering::class),
@@ -254,7 +266,10 @@ class VisitsStatsHelperTest extends TestCase
     #[Test]
     public function nonOrphanVisitsAreReturnedAsExpected(): void
     {
-        $list = map(range(0, 3), fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()));
+        $list = array_map(
+            static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
+            range(0, 3),
+        );
         $repo = $this->createMock(VisitRepository::class);
         $repo->expects($this->once())->method('countNonOrphanVisits')->with(
             $this->isInstanceOf(VisitsCountFiltering::class),
diff --git a/module/Rest/config/access-logs.config.php b/module/Rest/config/access-logs.config.php
index 1f0dd0e8..def1a93a 100644
--- a/module/Rest/config/access-logs.config.php
+++ b/module/Rest/config/access-logs.config.php
@@ -11,7 +11,7 @@ use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
 return [
 
     'access_logs' => [
-        'ignored_paths' => [
+        'ignored_path_prefixes' => [
             Action\HealthAction::ROUTE_PATH,
         ],
     ],
@@ -20,7 +20,7 @@ return [
     ConfigAbstractFactory::class => [
         // Use MergeReplaceKey to overwrite what was defined in shlink-common, instead of merging it
         AccessLogMiddleware::class => new MergeReplaceKey(
-            [AccessLogMiddleware::LOGGER_SERVICE_NAME, 'config.access_logs.ignored_paths'],
+            [AccessLogMiddleware::LOGGER_SERVICE_NAME, 'config.access_logs.ignored_path_prefixes'],
         ),
     ],
 
diff --git a/module/Rest/src/Action/Tag/ListTagsAction.php b/module/Rest/src/Action/Tag/ListTagsAction.php
index 34f44475..9674d5bc 100644
--- a/module/Rest/src/Action/Tag/ListTagsAction.php
+++ b/module/Rest/src/Action/Tag/ListTagsAction.php
@@ -14,7 +14,7 @@ use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
 use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
 use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
 
-use function Functional\map;
+use function array_map;
 
 class ListTagsAction extends AbstractRestAction
 {
@@ -41,7 +41,7 @@ class ListTagsAction extends AbstractRestAction
         // This part is deprecated. To get tags with stats, the /tags/stats endpoint should be used instead
         $tagsInfo = $this->tagService->tagsInfo($params, $apiKey);
         $rawTags = $this->serializePaginator($tagsInfo, dataProp: 'stats');
-        $rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag);
+        $rawTags['data'] = array_map(static fn (TagInfo $info) => $info->tag, [...$tagsInfo]);
 
         return new JsonResponse(['tags' => $rawTags]);
     }
diff --git a/module/Rest/src/ConfigProvider.php b/module/Rest/src/ConfigProvider.php
index 215a4d6e..7c57d8b1 100644
--- a/module/Rest/src/ConfigProvider.php
+++ b/module/Rest/src/ConfigProvider.php
@@ -4,8 +4,8 @@ declare(strict_types=1);
 
 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 @@ class ConfigProvider
     public static function applyRoutesPrefix(array $routes): array
     {
         $healthRoute = self::buildUnversionedHealthRouteFromExistingRoutes($routes);
-        $prefixedRoutes = map($routes, static function (array $route) {
+        $prefixedRoutes = array_map(static function (array $route) {
             ['path' => $path] = $route;
             $route['path'] = sprintf('%s%s', self::ROUTES_PREFIX, $path);
             return $route;
-        });
+        }, $routes);
 
         return $healthRoute !== null ? [...$prefixedRoutes, $healthRoute] : $prefixedRoutes;
     }
diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php
index 78f738a3..01592129 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\DataProvider;
 use PHPUnit\Framework\Attributes\Test;
 use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
 
-use function Functional\map;
+use function array_map;
 use function range;
 use function sprintf;
 
@@ -108,7 +108,7 @@ class CreateShortUrlTest extends ApiTestCase
 
     public static function provideMaxVisits(): array
     {
-        return map(range(10, 15), fn(int $i) => [$i]);
+        return array_map(static fn (int $i) => [$i], range(10, 15));
     }
 
     #[Test]

From 549c6605f0c2ebab0573f632df6d553a184ac342 Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandrocelaya@gmail.com>
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 0e1b996e..8071556e 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 58899217..44095656 100644
--- a/config/autoload/entity-manager.global.php
+++ b/config/autoload/entity-manager.global.php
@@ -5,11 +5,11 @@ declare(strict_types=1);
 use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
 use Shlinkio\Shlink\Core\Config\EnvVars;
 
-use function Functional\contains;
+use function Shlinkio\Shlink\Core\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 1beed0e3..75937bec 100644
--- a/config/test/test_config.global.php
+++ b/config/test/test_config.global.php
@@ -28,9 +28,9 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 use function file_exists;
-use function Functional\contains;
 use function Laminas\Stratigility\middleware;
 use function Shlinkio\Shlink\Config\env;
+use function Shlinkio\Shlink\Core\contains;
 use function sprintf;
 use function sys_get_temp_dir;
 
@@ -41,7 +41,7 @@ $isApiTest = env('TEST_ENV') === 'api';
 $isCliTest = env('TEST_ENV') === 'cli';
 $isE2eTest = $isApiTest || $isCliTest;
 $coverageType = env('GENERATE_COVERAGE');
-$generateCoverage = contains(['yes', 'pretty'], $coverageType);
+$generateCoverage = contains($coverageType, ['yes', 'pretty']);
 
 $coverage = null;
 if ($isE2eTest && $generateCoverage) {
diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
index c70e2f76..b9bb8f10 100644
--- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php
+++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
@@ -17,8 +17,7 @@ use Symfony\Component\Process\PhpExecutableFinder;
 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 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
         $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 f55f247d..3277f763 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\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
 use function array_map;
+use function array_unique;
 use function explode;
-use function Functional\curry;
-use function Functional\flatten;
-use function Functional\unique;
+use function Shlinkio\SHlink\Core\flatten;
 use function sprintf;
 
 class CreateShortUrlCommand extends Command
@@ -144,8 +143,8 @@ class CreateShortUrlCommand extends Command
             return ExitCode::EXIT_FAILURE;
         }
 
-        $explodeWithComma = curry(explode(...))(',');
-        $tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
+        $explodeWithComma = static fn (string $tag) => explode(',', $tag);
+        $tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
         $customSlug = $input->getOption('custom-slug');
         $maxVisits = $input->getOption('max-visits');
         $shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
index a247380e..8766ecc5 100644
--- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
+++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
@@ -19,9 +19,9 @@ use Symfony\Component\Console\Output\OutputInterface;
 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 @@ abstract class AbstractVisitsListCommand extends Command
             // 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 32d357e3..bcda4bb4 100644
--- a/module/Core/functions/functions.php
+++ b/module/Core/functions/functions.php
@@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Core;
 
 use BackedEnum;
 use Cake\Chronos\Chronos;
-use Cake\Chronos\ChronosInterface;
 use DateTimeInterface;
 use Doctrine\ORM\Mapping\Builder\FieldBuilder;
 use Jaybizzle\CrawlerDetect\CrawlerDetect;
@@ -18,8 +17,10 @@ use Shlinkio\Shlink\Common\Util\DateRange;
 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 306c2b44..51162d5f 100644
--- a/module/Core/src/Action/Model/QrCodeParams.php
+++ b/module/Core/src/Action/Model/QrCodeParams.php
@@ -18,7 +18,7 @@ use Endroid\QrCode\Writer\WriterInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Shlinkio\Shlink\Core\Options\QrCodeOptions;
 
-use function Functional\contains;
+use function Shlinkio\Shlink\Core\contains;
 use function strtolower;
 use function trim;
 
@@ -74,7 +74,7 @@ final class QrCodeParams
     private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface
     {
         $qFormat = self::normalizeParam($query['format'] ?? '');
-        $format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format);
+        $format = contains($qFormat, self::SUPPORTED_FORMATS) ? $qFormat : self::normalizeParam($defaults->format);
 
         return match ($format) {
             'svg' => new SvgWriter(),
diff --git a/module/Core/src/ShortUrl/Model/OrderableField.php b/module/Core/src/ShortUrl/Model/OrderableField.php
index ac1bc632..1b61a155 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 9fda1809..5694f6e1 100644
--- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php
+++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php
@@ -10,9 +10,9 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
 
 use function array_keys;
 use function array_values;
-use function Functional\contains;
 use function Functional\every;
 use function is_array;
+use function Shlinkio\Shlink\Core\contains;
 use function Shlinkio\Shlink\Core\enumValues;
 
 class DeviceLongUrlsValidator extends AbstractValidator
@@ -41,7 +41,7 @@ class DeviceLongUrlsValidator extends AbstractValidator
 
         $validValues = enumValues(DeviceType::class);
         $keys = array_keys($value);
-        if (! every($keys, static fn ($key) => contains($validValues, $key))) {
+        if (! every($keys, static fn ($key) => contains($key, $validValues))) {
             $this->error(self::INVALID_DEVICE);
             return false;
         }
diff --git a/module/Core/src/Util/RedirectStatus.php b/module/Core/src/Util/RedirectStatus.php
index 76c047f4..313dc432 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 f85a9d44..c4ca402a 100644
--- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php
+++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php
@@ -28,7 +28,7 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
 use Shlinkio\Shlink\Core\Visit\Model\Visitor;
 
 use function count;
-use function Functional\contains;
+use function Shlinkio\Shlink\Core\contains;
 
 class NotifyVisitToWebHooksTest extends TestCase
 {
@@ -102,7 +102,7 @@ class NotifyVisitToWebHooksTest extends TestCase
                 return true;
             }),
         )->willReturnCallback(function ($_, $webhook) use ($invalidWebhooks) {
-            $shouldReject = contains($invalidWebhooks, $webhook);
+            $shouldReject = contains($webhook, $invalidWebhooks);
             return $shouldReject ? new RejectedPromise(new Exception('')) : new FulfilledPromise('');
         });
         $this->logger->expects($this->exactly(count($invalidWebhooks)))->method('warning')->with(
diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php
index bf2896e2..5b174053 100644
--- a/module/Core/test/Importer/ImportedLinksProcessorTest.php
+++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php
@@ -32,8 +32,8 @@ use stdClass;
 use Symfony\Component\Console\Style\StyleInterface;
 
 use function count;
-use function Functional\contains;
 use function Functional\some;
+use function Shlinkio\Shlink\Core\contains;
 use function sprintf;
 use function str_contains;
 
@@ -128,8 +128,8 @@ class ImportedLinksProcessorTest extends TestCase
         $this->em->method('getRepository')->with(ShortUrl::class)->willReturn($this->repo);
         $this->repo->expects($this->exactly(count($urls)))->method('findOneByImportedUrl')->willReturnCallback(
             fn (ImportedShlinkUrl $url): ?ShortUrl => contains(
-                ['https://foo', 'https://baz2', 'https://baz3'],
                 $url->longUrl,
+                ['https://foo', 'https://baz2', 'https://baz3'],
             ) ? ShortUrl::fromImport($url, true) : null,
         );
         $this->shortCodeHelper->expects($this->exactly(2))->method('ensureShortCodeUniqueness')->willReturn(true);
diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php
index 7b911817..85ec61b7 100644
--- a/module/Rest/src/Middleware/AuthenticationMiddleware.php
+++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php
@@ -17,16 +17,16 @@ use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
 use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
 use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
 
-use function Functional\contains;
+use function Shlinkio\Shlink\Core\contains;
 
 class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface
 {
     public const API_KEY_HEADER = 'X-Api-Key';
 
     public function __construct(
-        private ApiKeyServiceInterface $apiKeyService,
-        private array $routesWithoutApiKey,
-        private array $routesWithQueryApiKey,
+        private readonly ApiKeyServiceInterface $apiKeyService,
+        private readonly array $routesWithoutApiKey,
+        private readonly array $routesWithQueryApiKey,
     ) {
     }
 
@@ -38,7 +38,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
             $routeResult === null
             || $routeResult->isFailure()
             || $request->getMethod() === self::METHOD_OPTIONS
-            || contains($this->routesWithoutApiKey, $routeResult->getMatchedRouteName())
+            || contains($routeResult->getMatchedRouteName(), $this->routesWithoutApiKey)
         ) {
             return $handler->handle($request);
         }
@@ -61,7 +61,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
     {
         $routeName = $routeResult->getMatchedRouteName();
         $query = $request->getQueryParams();
-        $isRouteWithApiKeyInQuery = contains($this->routesWithQueryApiKey, $routeName);
+        $isRouteWithApiKeyInQuery = contains($routeName, $this->routesWithQueryApiKey);
         $apiKey = $isRouteWithApiKeyInQuery ? ($query['apiKey'] ?? '') : $request->getHeaderLine(self::API_KEY_HEADER);
 
         if (empty($apiKey)) {
diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php
index c31bc268..b0548f97 100644
--- a/module/Rest/src/Middleware/BodyParserMiddleware.php
+++ b/module/Rest/src/Middleware/BodyParserMiddleware.php
@@ -12,7 +12,7 @@ use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use Shlinkio\Shlink\Core\Exception\MalformedBodyException;
 
-use function Functional\contains;
+use function Shlinkio\Shlink\Core\contains;
 use function Shlinkio\Shlink\Json\json_decode;
 
 class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
@@ -25,11 +25,11 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
         // In requests that do not allow body or if the body has already been parsed, continue to next middleware
         if (
             ! empty($currentParams)
-            || contains([
+            || contains($method, [
                 self::METHOD_GET,
                 self::METHOD_HEAD,
                 self::METHOD_OPTIONS,
-            ], $method)
+            ])
         ) {
             return $handler->handle($request);
         }

From bff4bd12ae08038bb6223bde0f8514d5bcb2650f Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandrocelaya@gmail.com>
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  | 26 +++++---------
 .../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, 156 insertions(+), 91 deletions(-)

diff --git a/composer.json b/composer.json
index 8071556e..e295539e 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 fb3b7961..bb497021 100644
--- a/data/migrations/Version20200105165647.php
+++ b/data/migrations/Version20200105165647.php
@@ -11,7 +11,7 @@ use Doctrine\DBAL\Schema\Schema;
 use Doctrine\DBAL\Types\Types;
 use Doctrine\Migrations\AbstractMigration;
 
-use function Functional\some;
+use function Shlinkio\Shlink\Core\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 830daf64..f5faba4e 100644
--- a/data/migrations/Version20200106215144.php
+++ b/data/migrations/Version20200106215144.php
@@ -7,11 +7,10 @@ namespace ShlinkMigrations;
 use Doctrine\DBAL\Exception;
 use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\Table;
 use Doctrine\DBAL\Types\Types;
 use Doctrine\Migrations\AbstractMigration;
 
-use function Functional\none;
-
 final class Version20200106215144 extends AbstractMigration
 {
     private const COLUMNS = ['latitude', 'longitude'];
@@ -22,16 +21,24 @@ final class Version20200106215144 extends AbstractMigration
     public function up(Schema $schema): void
     {
         $visitLocations = $schema->getTable('visit_locations');
-        $this->skipIf(none(
-            self::COLUMNS,
-            fn (string $oldColName) => $visitLocations->hasColumn($oldColName),
-        ), 'Old columns do not exist');
+        $this->skipIf($this->oldColumnsDoNotExist($visitLocations), 'Old columns do not exist');
 
         foreach (self::COLUMNS as $colName) {
             $visitLocations->dropColumn($colName);
         }
     }
 
+    public function oldColumnsDoNotExist(Table $visitLocations): bool
+    {
+        foreach (self::COLUMNS as $oldColName) {
+            if ($visitLocations->hasColumn($oldColName)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     /**
      * @throws Exception
      */
diff --git a/data/migrations/Version20200110182849.php b/data/migrations/Version20200110182849.php
index b267bfbc..4b608bb2 100644
--- a/data/migrations/Version20200110182849.php
+++ b/data/migrations/Version20200110182849.php
@@ -9,9 +9,6 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Schema;
 use Doctrine\Migrations\AbstractMigration;
 
-use function Functional\each;
-use function Functional\partial_left;
-
 final class Version20200110182849 extends AbstractMigration
 {
     private const DEFAULT_EMPTY_VALUE = '';
@@ -31,11 +28,11 @@ final class Version20200110182849 extends AbstractMigration
 
     public function up(Schema $schema): void
     {
-        each(
-            self::COLUMN_DEFAULTS_MAP,
-            fn (array $columns, string $tableName) =>
-                each($columns, partial_left([$this, 'setDefaultValueForColumnInTable'], $tableName)),
-        );
+        foreach (self::COLUMN_DEFAULTS_MAP as $tableName => $columns) {
+            foreach ($columns as $columnName) {
+                $this->setDefaultValueForColumnInTable($tableName, $columnName);
+            }
+        }
     }
 
     /**
diff --git a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php
index 4a3f8062..bf08e7f3 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\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
-use function Functional\filter;
-use function Functional\invoke;
+use function array_filter;
+use function array_map;
 use function sprintf;
 use function str_contains;
 
@@ -23,7 +23,7 @@ class DomainRedirectsCommand extends Command
 {
     public const NAME = 'domain:redirects';
 
-    public function __construct(private DomainServiceInterface $domainService)
+    public function __construct(private readonly DomainServiceInterface $domainService)
     {
         parent::__construct();
     }
@@ -52,9 +52,9 @@ class DomainRedirectsCommand extends Command
         $askNewDomain = static fn () => $io->ask('Domain authority for which you want to set specific redirects');
 
         /** @var string[] $availableDomains */
-        $availableDomains = invoke(
-            filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
-            'toString',
+        $availableDomains = array_map(
+            static fn (DomainItem $item) => $item->toString(),
+            array_filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
         );
         if (empty($availableDomains)) {
             $input->setArgument('domain', $askNewDomain());
diff --git a/module/CLI/src/Util/ShlinkTable.php b/module/CLI/src/Util/ShlinkTable.php
index cd38e5cd..c421c613 100644
--- a/module/CLI/src/Util/ShlinkTable.php
+++ b/module/CLI/src/Util/ShlinkTable.php
@@ -8,30 +8,30 @@ use Symfony\Component\Console\Helper\Table;
 use Symfony\Component\Console\Helper\TableSeparator;
 use Symfony\Component\Console\Output\OutputInterface;
 
-use function Functional\intersperse;
+use function array_pop;
 
 final class ShlinkTable
 {
     private const DEFAULT_STYLE_NAME = 'default';
     private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
 
-    private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators)
+    private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators = false)
     {
     }
 
     public static function default(OutputInterface $output): self
     {
-        return new self(new Table($output), false);
+        return new self(new Table($output));
     }
 
     public static function withRowSeparators(OutputInterface $output): self
     {
-        return new self(new Table($output), true);
+        return new self(new Table($output), withRowSeparators: true);
     }
 
     public static function fromBaseTable(Table $baseTable): self
     {
-        return new self($baseTable, false);
+        return new self($baseTable);
     }
 
     public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
@@ -39,7 +39,7 @@ final class ShlinkTable
         $style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
         $style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
               ->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
-        $tableRows = $this->withRowSeparators ? intersperse($rows, new TableSeparator()) : $rows;
+        $tableRows = $this->withRowSeparators ? $this->addRowSeparators($rows) : $rows;
 
         $table = clone $this->baseTable;
         $table->setStyle($style)
@@ -49,4 +49,20 @@ final class ShlinkTable
               ->setHeaderTitle($headerTitle)
               ->render();
     }
+
+    private function addRowSeparators(array $rows): array
+    {
+        $aggregation = [];
+        $separator = new TableSeparator();
+
+        foreach ($rows as $row) {
+            $aggregation[] = $row;
+            $aggregation[] = $separator;
+        }
+
+        // Remove last separator
+        array_pop($aggregation);
+
+        return $aggregation;
+    }
 }
diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php
index bcda4bb4..f13fc670 100644
--- a/module/Core/functions/functions.php
+++ b/module/Core/functions/functions.php
@@ -16,10 +16,10 @@ use PUGX\Shortid\Factory as ShortIdFactory;
 use Shlinkio\Shlink\Common\Util\DateRange;
 use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
 
+use function array_keys;
 use function array_map;
 use function array_reduce;
 use function date_default_timezone_get;
-use function Functional\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 3ab2e740..540956ee 100644
--- a/module/Core/src/Config/NotFoundRedirectResolver.php
+++ b/module/Core/src/Config/NotFoundRedirectResolver.php
@@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
 use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
 
 use function Functional\compose;
-use function Functional\id;
 use function str_replace;
 use function urlencode;
 
@@ -23,8 +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 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
             $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 05ecdb6c..42f00889 100644
--- a/module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php
+++ b/module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php
@@ -9,25 +9,34 @@ use Mezzio\Router\Route;
 use Shlinkio\Shlink\Core\Action\RedirectAction;
 use Shlinkio\Shlink\Core\Util\RedirectStatus;
 
-use function array_values;
-use function count;
-use function Functional\partition;
-
 use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
 
+/**
+ * Sets the appropriate allowed methods on the redirect route, based on the redirect status code that was configured.
+ *  * For "legacy" status codes (301 and 302) the redirect URL will work only on GET method.
+ *  * For other status codes (307 and 308) the redirect URL will work on any method.
+ */
 class ShortUrlMethodsProcessor
 {
     public function __invoke(array $config): array
     {
-        [$redirectRoutes, $rest] = partition(
-            $config['routes'] ?? [],
-            static fn (array $route) => $route['name'] === RedirectAction::class,
-        );
-        if (count($redirectRoutes) === 0) {
+        $allRoutes = $config['routes'] ?? [];
+        $redirectRoute = null;
+        $rest = [];
+
+        // Get default route from routes array
+        foreach ($allRoutes as $route) {
+            if ($route['name'] === RedirectAction::class) {
+                $redirectRoute ??= $route;
+            } else {
+                $rest[] = $route;
+            }
+        }
+
+        if ($redirectRoute === null) {
             return $config;
         }
 
-        [$redirectRoute] = array_values($redirectRoutes);
         $redirectStatus = RedirectStatus::tryFrom(
             $config['redirects']['redirect_status_code'] ?? 0,
         ) ?? DEFAULT_REDIRECT_STATUS_CODE;
diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php
index 9aa4e3d0..93adbf5f 100644
--- a/module/Core/src/Domain/DomainService.php
+++ b/module/Core/src/Domain/DomainService.php
@@ -15,8 +15,6 @@ use Shlinkio\Shlink\Rest\ApiKey\Role;
 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 @@ class DomainService implements DomainServiceInterface
     {
         /** @var DomainRepositoryInterface $repo */
         $repo = $this->em->getRepository(Domain::class);
-        $groups = group(
-            $repo->findDomains($apiKey),
-            fn (Domain $domain) => $domain->authority === $this->defaultDomain ? 'default' : 'domains',
-        );
+        $allDomains = $repo->findDomains($apiKey);
+        $defaultDomain = null;
+        $restOfDomains = [];
 
-        return [first($groups['default'] ?? []), $groups['domains'] ?? []];
+        foreach ($allDomains as $domain) {
+            if ($domain->authority === $this->defaultDomain) {
+                $defaultDomain = $domain;
+            } else {
+                $restOfDomains[] = $domain;
+            }
+        }
+
+        return [$defaultDomain, $restOfDomains];
     }
 
     /**
diff --git a/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php b/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php
index dae9130f..3ec9417c 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\EventDispatcher\PublishingUpdatesGeneratorInterface;
 use Shlinkio\Shlink\Core\Visit\Entity\Visit;
 use Throwable;
 
-use function Functional\each;
+use function array_walk;
 
 abstract class AbstractNotifyVisitListener extends AbstractAsyncListener
 {
@@ -46,7 +46,7 @@ abstract class AbstractNotifyVisitListener extends AbstractAsyncListener
         $updates = $this->determineUpdatesForVisit($visit);
 
         try {
-            each($updates, fn (Update $update) => $this->publishingHelper->publishUpdate($update));
+            array_walk($updates, fn (Update $update) => $this->publishingHelper->publishUpdate($update));
         } catch (Throwable $e) {
             $this->logger->debug(
                 'Error while trying to notify {name} with new visit. {e}',
diff --git a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
index c7b1efc0..a83ec01d 100644
--- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
+++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
@@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
 
 use Shlinkio\Shlink\Core\Model\DeviceType;
 
-use function Functional\group;
 use function trim;
 
 final class DeviceLongUrlPair
@@ -25,27 +24,20 @@ final class DeviceLongUrlPair
      *  * The first one is a list of mapped instances for those entries in the map with non-null value
      *  * The second is a list of DeviceTypes which have been provided with value null
      *
-     * @param array<string, string> $map
+     * @param array<string, string | null> $map
      * @return array{array<string, self>, DeviceType[]}
      */
     public static function fromMapToChangeSet(array $map): array
     {
-        $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 = [];
-        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);
+        $deviceTypesToRemove = [];
+
+        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 5694f6e1..0c3b19c2 100644
--- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php
+++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php
@@ -10,10 +10,10 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
 
 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 d74da44a..ce8b1f76 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\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
 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 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
         $nonBotVisitsSubQb = $buildVisitsSubQb(true, 'non_bot_visits');
 
         // Apply API key specification to all sub-queries
-        each([$tagsSubQb, $allVisitsSubQb, $nonBotVisitsSubQb], $applyApiKeyToNativeQb);
+        $queryBuilders = [$tagsSubQb, $allVisitsSubQb, $nonBotVisitsSubQb];
+        array_walk($queryBuilders, $applyApiKeyToNativeQb);
 
         // A native query builder needs to be used here, because DQL and ORM query builders do not support
         // sub-queries at "from" and "join" level.
diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php
index 0002d3b1..e722bf25 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\Model\Visitor;
 use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer;
 use Throwable;
 
+use function array_walk;
 use function count;
-use function Functional\each;
-use function Functional\noop;
 
 class NotifyVisitToRabbitMqTest extends TestCase
 {
@@ -77,7 +76,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
     {
         $visitId = '123';
         $this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit);
-        each($expectedChannels, function (string $method): void {
+        array_walk($expectedChannels, function (string $method): void {
             $this->updatesGenerator->expects($this->once())->method($method)->with(
                 $this->isInstanceOf(Visit::class),
             )->willReturn(Update::forTopicAndPayload('', []));
@@ -153,7 +152,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
         yield 'legacy non-orphan visit' => [
             true,
             $visit = Visit::forValidShortUrl(ShortUrl::withLongUrl('https://longUrl'), Visitor::emptyInstance()),
-            noop(...),
+            static fn () => null,
             function (MockObject & PublishingHelperInterface $helper) use ($visit): void {
                 $helper->method('publishUpdate')->with(self::callback(function (Update $update) use ($visit): bool {
                     $payload = $update->payload;
@@ -170,7 +169,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
         yield 'legacy orphan visit' => [
             true,
             Visit::forBasePath(Visitor::emptyInstance()),
-            noop(...),
+            static fn () => null,
             function (MockObject & PublishingHelperInterface $helper): void {
                 $helper->method('publishUpdate')->with(self::callback(function (Update $update): bool {
                     $payload = $update->payload;
diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php
index 5b174053..2cdbf654 100644
--- a/module/Core/test/Importer/ImportedLinksProcessorTest.php
+++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php
@@ -32,8 +32,8 @@ use stdClass;
 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 c1d66e61..ba6fab58 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\Model\ImportedShlinkUrl;
 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 b43eed91..b05ab7d9 100644
--- a/module/Core/test/ShortUrl/Middleware/TrimTrailingSlashMiddlewareTest.php
+++ b/module/Core/test/ShortUrl/Middleware/TrimTrailingSlashMiddlewareTest.php
@@ -16,9 +16,6 @@ use Psr\Http\Server\RequestHandlerInterface;
 use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
 use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
 
-use function Functional\compose;
-use function Functional\const_function;
-
 class TrimTrailingSlashMiddlewareTest extends TestCase
 {
     private MockObject & RequestHandlerInterface $requestHandler;
@@ -34,7 +31,10 @@ class TrimTrailingSlashMiddlewareTest extends TestCase
         ServerRequestInterface $inputRequest,
         callable $assertions,
     ): void {
-        $arg = compose($assertions, const_function(true));
+        $arg = static function (...$args) use ($assertions): bool {
+            $assertions(...$args);
+            return true;
+        };
         $this->requestHandler->expects($this->once())->method('handle')->with($this->callback($arg))->willReturn(
             new Response(),
         );
diff --git a/module/Rest/src/ConfigProvider.php b/module/Rest/src/ConfigProvider.php
index 7c57d8b1..067c6952 100644
--- a/module/Rest/src/ConfigProvider.php
+++ b/module/Rest/src/ConfigProvider.php
@@ -4,8 +4,9 @@ declare(strict_types=1);
 
 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 @@ class ConfigProvider
 
     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 685d3795..8cfb918c 100644
--- a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php
+++ b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php
@@ -15,8 +15,8 @@ use Shlinkio\Shlink\Core\Exception\TagConflictException;
 use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
 use Shlinkio\Shlink\Core\Exception\ValidationException;
 
+use function end;
 use function explode;
-use function Functional\last;
 
 /** @deprecated */
 class BackwardsCompatibleProblemDetailsException extends RuntimeException implements ProblemDetailsExceptionInterface
@@ -77,7 +77,9 @@ class BackwardsCompatibleProblemDetailsException extends RuntimeException implem
 
     private function remapType(string $wrappedType): string
     {
-        $lastSegment = last(explode('/', $wrappedType));
+        $segments = explode('/', $wrappedType);
+        $lastSegment = end($segments);
+
         return match ($lastSegment) {
             ValidationException::ERROR_CODE => 'INVALID_ARGUMENT',
             DeleteShortUrlException::ERROR_CODE => 'INVALID_SHORT_URL_DELETION',

From 1854cc2f19fadbcf6a1169d736e4f63176ad0ffc Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandrocelaya@gmail.com>
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 2b0d0d1a..3b483ece 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 e295539e..6d013e09 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 44095656..849c91af 100644
--- a/config/autoload/entity-manager.global.php
+++ b/config/autoload/entity-manager.global.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
 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 75937bec..8ae64d7a 100644
--- a/config/test/test_config.global.php
+++ b/config/test/test_config.global.php
@@ -30,7 +30,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 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 bb497021..26f8cc0a 100644
--- a/data/migrations/Version20200105165647.php
+++ b/data/migrations/Version20200105165647.php
@@ -11,7 +11,7 @@ use Doctrine\DBAL\Schema\Schema;
 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 b9bb8f10..53b854d1 100644
--- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php
+++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
@@ -17,7 +17,8 @@ use Symfony\Component\Process\PhpExecutableFinder;
 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 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
         $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 3277f763..64418aa6 100644
--- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
+++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
@@ -22,7 +22,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
 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 8766ecc5..a15eb5e7 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\Command\Command;
 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 @@ abstract class AbstractVisitsListCommand extends Command
             ];
 
             // 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 00000000..5fb636e6
--- /dev/null
+++ b/module/Core/functions/array-utils.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shlinkio\Shlink\Core\ArrayUtils;
+
+use function array_filter;
+use function array_reduce;
+use function in_array;
+
+use const ARRAY_FILTER_USE_KEY;
+
+function contains(mixed $value, array $array): bool
+{
+    return in_array($value, $array, strict: true);
+}
+
+/**
+ * @param array[] $multiArray
+ * @return array
+ */
+function flatten(array $multiArray): array
+{
+    return array_reduce(
+        $multiArray,
+        static fn (array $carry, array $value) => [...$carry, ...$value],
+        initial: [],
+    );
+}
+
+/**
+ * Checks if a callback returns true for at least one item in a collection.
+ * @param callable(mixed $value, mixed $key): bool $callback
+ */
+function some(iterable $collection, callable $callback): bool
+{
+    foreach ($collection as $key => $value) {
+        if ($callback($value, $key)) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+/**
+ * Checks if a callback returns true for all item in a collection.
+ * @param callable(mixed $value, string|number $key): bool $callback
+ */
+function every(iterable $collection, callable $callback): bool
+{
+    foreach ($collection as $key => $value) {
+        if (! $callback($value, $key)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+/**
+ * Returns an array containing only those entries in the array whose key is in the supplied keys.
+ */
+function select_keys(array $array, array $keys): array
+{
+    return array_filter(
+        $array,
+        static fn (string $key) => contains(
+            $key,
+            $keys,
+        ),
+        ARRAY_FILTER_USE_KEY,
+    );
+}
diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php
index f13fc670..d07bc9e2 100644
--- a/module/Core/functions/functions.php
+++ b/module/Core/functions/functions.php
@@ -20,7 +20,6 @@ use function array_keys;
 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 51162d5f..05181f20 100644
--- a/module/Core/src/Action/Model/QrCodeParams.php
+++ b/module/Core/src/Action/Model/QrCodeParams.php
@@ -18,7 +18,7 @@ use Endroid\QrCode\Writer\WriterInterface;
 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 540956ee..ce5401d2 100644
--- a/module/Core/src/Config/NotFoundRedirectResolver.php
+++ b/module/Core/src/Config/NotFoundRedirectResolver.php
@@ -12,7 +12,6 @@ use Psr\Log\LoggerInterface;
 use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
 use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
 
-use function Functional\compose;
 use function str_replace;
 use function urlencode;
 
@@ -51,9 +50,6 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
 
     private function resolvePlaceholders(UriInterface $currentUri, string $redirectUrl): string
     {
-        $domain = $currentUri->getAuthority();
-        $path = $currentUri->getPath();
-
         try {
             $redirectUri = Uri::createFromString($redirectUrl);
         } catch (SyntaxError $e) {
@@ -64,18 +60,32 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
             return $redirectUrl;
         }
 
-        $replacePlaceholderForPattern = static fn (string $pattern, string $replace, callable $modifier) =>
-            static fn (?string $value) =>
-                $value === null ? null : str_replace($modifier($pattern), $modifier($replace), $value);
-        $replacePlaceholders = static fn (callable $modifier) => compose(
-            $replacePlaceholderForPattern(self::DOMAIN_PLACEHOLDER, $domain, $modifier),
-            $replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier),
+        $path = $currentUri->getPath();
+        $domain = $currentUri->getAuthority();
+
+        $replacePlaceholderForPattern = static fn (string $pattern, string $replace, ?string $value): string|null =>
+            $value === null ? null : str_replace($pattern, $replace, $value);
+
+        $replacePlaceholders = static function (
+            callable $modifier,
+            ?string $value,
+        ) use (
+            $replacePlaceholderForPattern,
+            $path,
+            $domain,
+        ): string|null {
+            $value = $replacePlaceholderForPattern($modifier(self::DOMAIN_PLACEHOLDER), $modifier($domain), $value);
+            return $replacePlaceholderForPattern($modifier(self::ORIGINAL_PATH_PLACEHOLDER), $modifier($path), $value);
+        };
+
+        $replacePlaceholdersInPath = static function (string $path) use ($replacePlaceholders): string {
+            $result = $replacePlaceholders(static fn (mixed $v) => $v, $path);
+            return str_replace('//', '/', $result ?? '');
+        };
+        $replacePlaceholdersInQuery = static fn (?string $query): string|null => $replacePlaceholders(
+            urlencode(...),
+            $query,
         );
-        $replacePlaceholdersInPath = compose(
-            $replacePlaceholders(static fn (mixed $v) => $v),
-            static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path),
-        );
-        $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 a83ec01d..a48c666b 100644
--- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
+++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php
@@ -24,7 +24,7 @@ final class DeviceLongUrlPair
      *  * The first one is a list of mapped instances for those entries in the map with non-null value
      *  * The second is a list of DeviceTypes which have been provided with value null
      *
-     * @param array<string, string | null> $map
+     * @param array<string, string|null> $map
      * @return array{array<string, self>, 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 1b61a155..685f6f12 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 0c3b19c2..82119e4e 100644
--- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php
+++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php
@@ -11,9 +11,9 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
 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 313dc432..f561e212 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 e8647165..1a6b04f9 100644
--- a/module/Core/src/Visit/RequestTracker.php
+++ b/module/Core/src/Visit/RequestTracker.php
@@ -20,6 +20,7 @@ use function array_keys;
 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 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
         $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 c4ca402a..8b9c10ac 100644
--- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php
+++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php
@@ -28,7 +28,7 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
 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 2cdbf654..921273c1 100644
--- a/module/Core/test/Importer/ImportedLinksProcessorTest.php
+++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php
@@ -32,8 +32,8 @@ use stdClass;
 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 ba6fab58..0a898399 100644
--- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php
+++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php
@@ -21,7 +21,7 @@ use Shlinkio\Shlink\Importer\Sources\ImportSource;
 
 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 85ec61b7..cf73ba10 100644
--- a/module/Rest/src/Middleware/AuthenticationMiddleware.php
+++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php
@@ -17,7 +17,7 @@ use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
 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 b0548f97..cdab8299 100644
--- a/module/Rest/src/Middleware/BodyParserMiddleware.php
+++ b/module/Rest/src/Middleware/BodyParserMiddleware.php
@@ -12,7 +12,7 @@ use Psr\Http\Server\MiddlewareInterface;
 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