diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b0d0d1ae..3b483eced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] +### Added +* *Nothing* + +### Changed +* Remove dependency on functional-php library + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [3.7.0] - 2023-11-25 ### Added * [#1798](https://github.com/shlinkio/shlink/issues/1798) Experimental support to send visits to an external Matomo instance. diff --git a/composer.json b/composer.json index e295539ef..6d013e091 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,6 @@ "laminas/laminas-servicemanager": "^3.21", "laminas/laminas-stdlib": "^3.17", "league/uri": "^6.8", - "lstrojny/functional-php": "^1.17", "matomo/matomo-php-tracker": "^3.2", "mezzio/mezzio": "^3.17", "mezzio/mezzio-fastroute": "^3.10", @@ -91,6 +90,7 @@ }, "files": [ "config/constants.php", + "module/Core/functions/array-utils.php", "module/Core/functions/functions.php" ] }, diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php index 440956560..849c91af0 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -5,7 +5,7 @@ use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Shlinkio\Shlink\Core\Config\EnvVars; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; return (static function (): array { $driver = EnvVars::DB_DRIVER->loadFromEnv(); diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 75937bece..8ae64d7ae 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -30,7 +30,7 @@ use function file_exists; use function Laminas\Stratigility\middleware; use function Shlinkio\Shlink\Config\env; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function sprintf; use function sys_get_temp_dir; diff --git a/data/migrations/Version20200105165647.php b/data/migrations/Version20200105165647.php index bb4970219..26f8cc0a7 100644 --- a/data/migrations/Version20200105165647.php +++ b/data/migrations/Version20200105165647.php @@ -11,7 +11,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; -use function Shlinkio\Shlink\Core\some; +use function Shlinkio\Shlink\Core\ArrayUtils\some; final class Version20200105165647 extends AbstractMigration { @@ -23,12 +23,11 @@ final class Version20200105165647 extends AbstractMigration public function preUp(Schema $schema): void { $visitLocations = $schema->getTable('visit_locations'); - $this->skipIf(some( - self::COLUMNS, - fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName), + $this->skipIf(some( + self::COLUMNS, + fn (string $v, string|int $newColName) => $visitLocations->hasColumn((string) $newColName), ), 'New columns already exist'); - foreach (self::COLUMNS as $columnName) { $qb = $this->connection->createQueryBuilder(); $qb->update('visit_locations') diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php index b9bb8f104..53b854d1d 100644 --- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php +++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php @@ -17,7 +17,8 @@ use Throwable; use function array_map; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\some; class CreateDatabaseCommand extends AbstractDatabaseCommand { @@ -71,15 +72,9 @@ private function databaseTablesExist(): bool $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); $shlinkTables = array_map(static fn (ClassMetadata $metadata) => $metadata->getTableName(), $allMetadata); - foreach ($shlinkTables as $shlinkTable) { - // If at least one of the shlink tables exist, we will consider the database exists somehow. - // Any other inconsistency will be taken care of by the migrations. - if (contains($shlinkTable, $existingTables)) { - return true; - } - } - - return false; + // If at least one of the shlink tables exist, we will consider the database exists somehow. + // Any other inconsistency will be taken care of by the migrations. + return some($shlinkTables, static fn (string $shlinkTable) => contains($shlinkTable, $existingTables)); } private function ensureDatabaseExistsAndGetTables(): array diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index 3277f7634..64418aa62 100644 --- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -22,7 +22,7 @@ use function array_map; use function array_unique; use function explode; -use function Shlinkio\SHlink\Core\flatten; +use function Shlinkio\Shlink\Core\ArrayUtils\flatten; use function sprintf; class CreateShortUrlCommand extends Command diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php index 8766ecc55..a15eb5e72 100644 --- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -16,14 +16,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use function array_filter; use function array_keys; use function array_map; use function Shlinkio\Shlink\Common\buildDateRange; +use function Shlinkio\Shlink\Core\ArrayUtils\select_keys; use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly; -use function Shlinkio\Shlink\Core\contains; - -use const ARRAY_FILTER_USE_KEY; abstract class AbstractVisitsListCommand extends Command { @@ -64,14 +61,7 @@ private function resolveRowsAndHeaders(Paginator $paginator): array ]; // Filter out unknown keys - return array_filter( - $rowData, - static fn (string $key) => contains( - $key, - ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys], - ), - ARRAY_FILTER_USE_KEY, - ); + return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); }, [...$paginator->getCurrentPageResults()]); $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); diff --git a/module/Core/functions/array-utils.php b/module/Core/functions/array-utils.php new file mode 100644 index 000000000..5fb636e69 --- /dev/null +++ b/module/Core/functions/array-utils.php @@ -0,0 +1,74 @@ + [...$carry, ...$value], + initial: [], + ); +} + +/** + * Checks if a callback returns true for at least one item in a collection. + * @param callable(mixed $value, mixed $key): bool $callback + */ +function some(iterable $collection, callable $callback): bool +{ + foreach ($collection as $key => $value) { + if ($callback($value, $key)) { + return true; + } + } + + return false; +} + +/** + * Checks if a callback returns true for all item in a collection. + * @param callable(mixed $value, string|number $key): bool $callback + */ +function every(iterable $collection, callable $callback): bool +{ + foreach ($collection as $key => $value) { + if (! $callback($value, $key)) { + return false; + } + } + + return true; +} + +/** + * Returns an array containing only those entries in the array whose key is in the supplied keys. + */ +function select_keys(array $array, array $keys): array +{ + return array_filter( + $array, + static fn (string $key) => contains( + $key, + $keys, + ), + ARRAY_FILTER_USE_KEY, + ); +} diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index f13fc670f..d07bc9e21 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -20,7 +20,6 @@ use function array_map; use function array_reduce; use function date_default_timezone_get; -use function in_array; use function is_array; use function print_r; use function Shlinkio\Shlink\Common\buildDateRange; @@ -183,51 +182,3 @@ function enumValues(string $enum): array $cache[$enum] = array_map(static fn (BackedEnum $type) => (string) $type->value, $enum::cases()) ); } - -function contains(mixed $value, array $array): bool -{ - return in_array($value, $array, strict: true); -} - -/** - * @param array[] $multiArray - * @return array - */ -function flatten(array $multiArray): array -{ - return array_reduce( - $multiArray, - static fn (array $carry, array $value) => [...$carry, ...$value], - initial: [], - ); -} - -/** - * Checks if a callback returns true for at least one item in a collection. - * @param callable(mixed $value, string|number $key): bool $callback - */ -function some(iterable $collection, callable $callback): bool -{ - foreach ($collection as $key => $value) { - if ($callback($value, $key)) { - return true; - } - } - - return false; -} - -/** - * Checks if a callback returns true for all item in a collection. - * @param callable(mixed $value, string|number $key): bool $callback - */ -function every(iterable $collection, callable $callback): bool -{ - foreach ($collection as $key => $value) { - if (! $callback($value, $key)) { - return false; - } - } - - return true; -} diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 51162d5f3..05181f20b 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -18,7 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Options\QrCodeOptions; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function strtolower; use function trim; diff --git a/module/Core/src/Config/NotFoundRedirectResolver.php b/module/Core/src/Config/NotFoundRedirectResolver.php index 540956ee7..ce5401d2e 100644 --- a/module/Core/src/Config/NotFoundRedirectResolver.php +++ b/module/Core/src/Config/NotFoundRedirectResolver.php @@ -12,7 +12,6 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType; use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface; -use function Functional\compose; use function str_replace; use function urlencode; @@ -51,9 +50,6 @@ public function resolveRedirectResponse( private function resolvePlaceholders(UriInterface $currentUri, string $redirectUrl): string { - $domain = $currentUri->getAuthority(); - $path = $currentUri->getPath(); - try { $redirectUri = Uri::createFromString($redirectUrl); } catch (SyntaxError $e) { @@ -64,18 +60,32 @@ private function resolvePlaceholders(UriInterface $currentUri, string $redirectU return $redirectUrl; } - $replacePlaceholderForPattern = static fn (string $pattern, string $replace, callable $modifier) => - static fn (?string $value) => - $value === null ? null : str_replace($modifier($pattern), $modifier($replace), $value); - $replacePlaceholders = static fn (callable $modifier) => compose( - $replacePlaceholderForPattern(self::DOMAIN_PLACEHOLDER, $domain, $modifier), - $replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier), - ); - $replacePlaceholdersInPath = compose( - $replacePlaceholders(static fn (mixed $v) => $v), - static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), + $path = $currentUri->getPath(); + $domain = $currentUri->getAuthority(); + + $replacePlaceholderForPattern = static fn (string $pattern, string $replace, ?string $value): string|null => + $value === null ? null : str_replace($pattern, $replace, $value); + + $replacePlaceholders = static function ( + callable $modifier, + ?string $value, + ) use ( + $replacePlaceholderForPattern, + $path, + $domain, + ): string|null { + $value = $replacePlaceholderForPattern($modifier(self::DOMAIN_PLACEHOLDER), $modifier($domain), $value); + return $replacePlaceholderForPattern($modifier(self::ORIGINAL_PATH_PLACEHOLDER), $modifier($path), $value); + }; + + $replacePlaceholdersInPath = static function (string $path) use ($replacePlaceholders): string { + $result = $replacePlaceholders(static fn (mixed $v) => $v, $path); + return str_replace('//', '/', $result ?? ''); + }; + $replacePlaceholdersInQuery = static fn (?string $query): string|null => $replacePlaceholders( + urlencode(...), + $query, ); - $replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...)); return $redirectUri ->withPath($replacePlaceholdersInPath($redirectUri->getPath())) diff --git a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php index a83ec01d9..a48c666bd 100644 --- a/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php +++ b/module/Core/src/ShortUrl/Model/DeviceLongUrlPair.php @@ -24,7 +24,7 @@ public static function fromRawTypeAndLongUrl(string $type, string $longUrl): sel * * The first one is a list of mapped instances for those entries in the map with non-null value * * The second is a list of DeviceTypes which have been provided with value null * - * @param array $map + * @param array $map * @return array{array, DeviceType[]} */ public static function fromMapToChangeSet(array $map): array diff --git a/module/Core/src/ShortUrl/Model/OrderableField.php b/module/Core/src/ShortUrl/Model/OrderableField.php index 1b61a1557..685f6f12a 100644 --- a/module/Core/src/ShortUrl/Model/OrderableField.php +++ b/module/Core/src/ShortUrl/Model/OrderableField.php @@ -2,7 +2,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; enum OrderableField: string { diff --git a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php index 0c3b19c24..82119e4e9 100644 --- a/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php +++ b/module/Core/src/ShortUrl/Model/Validation/DeviceLongUrlsValidator.php @@ -11,9 +11,9 @@ use function array_keys; use function array_values; use function is_array; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\every; use function Shlinkio\Shlink\Core\enumValues; -use function Shlinkio\Shlink\Core\every; class DeviceLongUrlsValidator extends AbstractValidator { diff --git a/module/Core/src/Util/RedirectStatus.php b/module/Core/src/Util/RedirectStatus.php index 313dc4325..f561e2125 100644 --- a/module/Core/src/Util/RedirectStatus.php +++ b/module/Core/src/Util/RedirectStatus.php @@ -2,7 +2,7 @@ namespace Shlinkio\Shlink\Core\Util; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; enum RedirectStatus: int { diff --git a/module/Core/src/Visit/RequestTracker.php b/module/Core/src/Visit/RequestTracker.php index e86471658..1a6b04f99 100644 --- a/module/Core/src/Visit/RequestTracker.php +++ b/module/Core/src/Visit/RequestTracker.php @@ -20,6 +20,7 @@ use function array_map; use function explode; use function implode; +use function Shlinkio\Shlink\Core\ArrayUtils\some; use function str_contains; class RequestTracker implements RequestTrackerInterface, RequestMethodInterface @@ -85,17 +86,13 @@ private function shouldDisableTrackingFromAddress(?string $remoteAddr): bool $remoteAddrParts = explode('.', $remoteAddr); $disableTrackingFrom = $this->trackingOptions->disableTrackingFrom; - foreach ($disableTrackingFrom as $value) { + return some($disableTrackingFrom, function (string $value) use ($ip, $remoteAddrParts): bool { $range = str_contains($value, '*') ? $this->parseValueWithWildcards($value, $remoteAddrParts) : Factory::parseRangeString($value); - if ($range !== null && $ip->matches($range)) { - return true; - } - } - - return false; + return $range !== null && $ip->matches($range); + }); } private function parseValueWithWildcards(string $value, array $remoteAddrParts): ?RangeInterface diff --git a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php index c4ca402a3..8b9c10aca 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php @@ -28,7 +28,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor; use function count; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; class NotifyVisitToWebHooksTest extends TestCase { diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php index 2cdbf6544..921273c1e 100644 --- a/module/Core/test/Importer/ImportedLinksProcessorTest.php +++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php @@ -32,8 +32,8 @@ use Symfony\Component\Console\Style\StyleInterface; use function count; -use function Shlinkio\Shlink\Core\contains; -use function Shlinkio\Shlink\Core\some; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\some; use function sprintf; use function str_contains; diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index ba6fab586..0a898399c 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -21,7 +21,7 @@ use function array_map; use function range; -use function Shlinkio\Shlink\Core\every; +use function Shlinkio\Shlink\Core\ArrayUtils\every; use function strlen; use function strtolower; diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php index 85ec61b7c..cf73ba10c 100644 --- a/module/Rest/src/Middleware/AuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php @@ -17,7 +17,7 @@ use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface { diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php index b0548f975..cdab82995 100644 --- a/module/Rest/src/Middleware/BodyParserMiddleware.php +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -12,7 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Exception\MalformedBodyException; -use function Shlinkio\Shlink\Core\contains; +use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function Shlinkio\Shlink\Json\json_decode; class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface